refactor data storage model to viewmodels

This commit is contained in:
Owen LeJeune
2023-06-20 21:15:34 -04:00
parent 3c2839f608
commit 17e3ad32f0
38 changed files with 2062 additions and 1643 deletions

View File

@@ -0,0 +1,11 @@
package com.owenlejeune.tvtime.api
import retrofit2.Response
infix fun <T> Response<T>.storedIn(body: (T) -> Unit) {
if (isSuccessful) {
body()?.let {
body(it)
}
}
}

View File

@@ -4,7 +4,13 @@ import androidx.compose.ui.text.intl.Locale
import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.api.Client
import com.owenlejeune.tvtime.api.QueryParam
import com.owenlejeune.tvtime.api.tmdb.api.v3.*
import com.owenlejeune.tvtime.api.tmdb.api.v3.AccountApi
import com.owenlejeune.tvtime.api.tmdb.api.v3.AuthenticationApi
import com.owenlejeune.tvtime.api.tmdb.api.v3.ConfigurationApi
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesApi
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleApi
import com.owenlejeune.tvtime.api.tmdb.api.v3.SearchApi
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvApi
import com.owenlejeune.tvtime.api.tmdb.api.v4.AccountV4Api
import com.owenlejeune.tvtime.api.tmdb.api.v4.AuthenticationV4Api
import com.owenlejeune.tvtime.api.tmdb.api.v4.ListV4Api
@@ -53,10 +59,6 @@ class TmdbClient: KoinComponent {
return clientV4.create(AuthenticationV4Api::class.java)
}
fun createGuestSessionService(): GuestSessionApi {
return client.create(GuestSessionApi::class.java)
}
fun createAccountService(): AccountApi {
return client.create(AccountApi::class.java)
}

View File

@@ -1,17 +1,52 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
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
class AccountService {
class AccountService: KoinComponent {
private val accountService by lazy { TmdbClient().createAccountService() }
private val TAG = "AccountService"
private val accountService: AccountApi by inject()
suspend fun getAccountDetails(): Response<AccountDetails> {
return accountService.getAccountDetails()
}
suspend fun markAsFavorite(accountId: Int, body: MarkAsFavoriteBody) {
val response = accountService.markAsFavorite(accountId, body)
if (response.isSuccessful) {
Log.d(TAG, "Successfully marked as favourite")
} else {
Log.e(TAG, "Issue marking as favourite")
}
}
suspend fun addToWatchlist(accountId: Int, body: WatchlistBody) {
val response = accountService.addToWatchlist(accountId, body)
if (response.isSuccessful) {
Log.d(TAG, "Successfully added to watchlist")
} else {
Log.e(TAG, "Issue adding to watchlist")
}
}
// TODO - replace these with account states API calls
suspend fun getFavoriteMovies(accountId: Int, page: Int = 1): Response<FavoriteMediaResponse<FavoriteMovie>> {
return accountService.getFavoriteMovies(accountId, page)
}
@@ -20,10 +55,6 @@ class AccountService {
return accountService.getFavoriteTvShows(accountId, page)
}
suspend fun markAsFavorite(accountId: Int, body: MarkAsFavoriteBody): Response<StatusResponse> {
return accountService.markAsFavorite(accountId, body)
}
suspend fun getRatedMovies(accountId: Int, page: Int = 1): Response<RatedMediaResponse<RatedMovie>> {
return accountService.getRatedMovies(accountId, page)
}
@@ -44,8 +75,4 @@ class AccountService {
return accountService.getTvWatchlist(accountId, page)
}
suspend fun addToWatchlist(accountId: Int, body: WatchlistBody): Response<StatusResponse> {
return accountService.addToWatchlist(accountId, body)
}
}

View File

@@ -0,0 +1,118 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ConfigurationCountry
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ConfigurationDetails
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ConfigurationJob
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ConfigurationLanguage
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ConfigurationTimezone
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class ConfigurationService: KoinComponent {
companion object {
private const val TAG = "ConfigurationService"
}
private val service: ConfigurationApi by inject()
val detailsConfiguration = mutableStateOf(ConfigurationDetails.Empty)
val countriesConfiguration = mutableStateListOf<ConfigurationCountry>()
val jobsConfiguration = mutableStateListOf<ConfigurationJob>()
val languagesConfiguration = mutableStateListOf<ConfigurationLanguage>()
val primaryTranslationsConfiguration = mutableStateListOf<String>()
val timezonesConfiguration = mutableStateListOf<ConfigurationTimezone>()
suspend fun getDetailsConfiguration() {
val response = service.getDetailsConfiguration()
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got details configuration: $it")
detailsConfiguration.value = it
} ?: run {
Log.w(TAG, "Problem getting details configuration")
}
} else {
Log.e(TAG, "Issue getting details configuration")
}
}
suspend fun getCountriesConfiguration() {
val response = service.getCountriesConfiguration()
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got countries configuration: $it")
countriesConfiguration.clear()
countriesConfiguration.addAll(it)
} ?: run {
Log.w(TAG, "Problem getting countries configuration")
}
} else {
Log.e(TAG, "Issue getting counties configuration")
}
}
suspend fun getJobsConfiguration() {
val response = service.getJobsConfiguration()
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got jobs configuration: $it")
jobsConfiguration.clear()
jobsConfiguration.addAll(it)
} ?: run {
Log.w(TAG, "Problem getting jobs configuration")
}
} else {
Log.e(TAG, "Issue getting jobs configuration")
}
}
suspend fun getLanguagesConfiguration() {
val response = service.getLanguagesConfiguration()
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got languages configuration: $it")
languagesConfiguration.clear()
languagesConfiguration.addAll(it)
} ?: run {
Log.w(TAG, "Problem getting languages configuration")
}
} else {
Log.e(TAG, "Issue getting languages configuration")
}
}
suspend fun getPrimaryTranslationsConfiguration() {
val response = service.getPrimaryTranslationsConfiguration()
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got translations configuration: $it")
primaryTranslationsConfiguration.clear()
primaryTranslationsConfiguration.addAll(it)
} ?: run {
Log.w(TAG, "Problem getting translations configuration")
}
} else {
Log.e(TAG, "Issue getting translations configuration")
}
}
suspend fun getTimezonesConfiguration() {
val response = service.getTimezonesConfiguration()
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got timezone configuration: $it")
timezonesConfiguration.clear()
timezonesConfiguration.addAll(it)
} ?: run {
Log.w(TAG, "Problem getting timezone configuration")
}
} else {
Log.e(TAG, "Issue getting timezone configuration")
}
}
}

View File

@@ -5,26 +5,28 @@ import retrofit2.Response
interface DetailService {
suspend fun getById(id: Int): Response<out DetailedItem>
suspend fun getById(id: Int)
suspend fun getImages(id: Int): Response<ImageCollection>
suspend fun getImages(id: Int)
suspend fun getCastAndCrew(id: Int): Response<CastAndCrew>
suspend fun getCastAndCrew(id: Int)
suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse>
suspend fun getVideos(id: Int): Response<VideoResponse>
suspend fun getVideos(id: Int)
suspend fun getReviews(id: Int): Response<ReviewResponse>
suspend fun getReviews(id: Int)
suspend fun postRating(id: Int, ratingBody: RatingBody): Response<StatusResponse>
suspend fun postRating(id: Int, ratingBody: RatingBody)
suspend fun deleteRating(id: Int): Response<StatusResponse>
suspend fun deleteRating(id: Int)
suspend fun getKeywords(id: Int): Response<KeywordsResponse>
suspend fun getKeywords(id: Int)
suspend fun getWatchProviders(id: Int): Response<WatchProviderResponse>
suspend fun getWatchProviders(id: Int)
suspend fun getExternalIds(id: Int): Response<ExternalIds>
suspend fun getExternalIds(id: Int)
suspend fun getAccountStates(id: Int)
}

View File

@@ -1,22 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3
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 retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
interface GuestSessionApi {
@GET("guest_session/{session_id}/rated/movies")
suspend fun getRatedMovies(@Path("session_id") sessionId: String): Response<RatedMediaResponse<RatedMovie>>
@GET("guest_session/{session_id}/rated/tv")
suspend fun getRatedTvShows(@Path("session_id") sessionId: String): Response<RatedMediaResponse<RatedTv>>
@GET("guest_session/{session_id}/rated/tv/episodes")
suspend fun getRatedTvEpisodes(@Path("session_id") sessionId: String): Response<RatedMediaResponse<RatedEpisode>>
}

View File

@@ -1,26 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
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 retrofit2.Response
class GuestSessionService {
private val service by lazy { TmdbClient().createGuestSessionService() }
suspend fun getRatedMovies(sessionId: String): Response<RatedMediaResponse<RatedMovie>> {
return service.getRatedMovies(sessionId = sessionId)
}
suspend fun getRatedTvShows(sessionId: String): Response<RatedMediaResponse<RatedTv>> {
return service.getRatedTvShows(sessionId = sessionId)
}
suspend fun getRatedTvEpisodes(sessionId: String): Response<RatedMediaResponse<RatedEpisode>> {
return service.getRatedTvEpisodes(sessionId = sessionId)
}
}

View File

@@ -61,4 +61,7 @@ interface MoviesApi {
@GET("movie/{id}/external_ids")
suspend fun getExternalIds(@Path("id") id: Int): Response<ExternalIds>
@GET("movie/{id}/account_states")
suspend fun getAccountStates(@Path("id") id: Int, @Query("session_id") sessionId: String): Response<AccountStates>
}

View File

@@ -1,25 +1,146 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CastAndCrew
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedItem
import android.util.Log
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.owenlejeune.tvtime.api.storedIn
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ExternalIds
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePageResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KeywordsResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Keyword
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieReleaseResults
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatingBody
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ReviewResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Review
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResult
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Searchable
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.StatusResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.VideoResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviderResponse
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.utils.SessionManager
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response
import java.util.Collections
import java.util.Locale
class MoviesService: KoinComponent, DetailService, HomePageService {
private val movieService by lazy { TmdbClient().createMovieService() }
companion object {
private const val TAG = "MovieService"
}
private val movieService: MoviesApi by inject()
val detailMovies = Collections.synchronizedMap(mutableStateMapOf<Int, DetailedMovie>())
val images = Collections.synchronizedMap(mutableStateMapOf<Int, ImageCollection>())
val cast = Collections.synchronizedMap(mutableStateMapOf<Int, List<CastMember>>())
val crew = Collections.synchronizedMap(mutableStateMapOf<Int, List<CrewMember>>())
val videos = Collections.synchronizedMap(mutableStateMapOf<Int, List<Video>>())
val reviews = Collections.synchronizedMap(mutableStateMapOf<Int, List<Review>>())
val keywords = Collections.synchronizedMap(mutableStateMapOf<Int, List<Keyword>>())
val watchProviders = Collections.synchronizedMap(mutableStateMapOf<Int, WatchProviders>())
val externalIds = Collections.synchronizedMap(mutableStateMapOf<Int, ExternalIds>())
val releaseDates = Collections.synchronizedMap(mutableStateMapOf<Int, List<MovieReleaseResults.ReleaseDateResult>>())
val similar = Collections.synchronizedMap(mutableStateMapOf<Int, Flow<PagingData<TmdbItem>>>())
val accountStates = Collections.synchronizedMap(mutableStateMapOf<Int, AccountStates>())
override suspend fun getById(id: Int) {
movieService.getMovieById(id) storedIn { detailMovies[id] = it }
}
override suspend fun getImages(id: Int) {
movieService.getMovieImages(id) storedIn { images[id] = it }
}
override suspend fun getCastAndCrew(id: Int) {
movieService.getCastAndCrew(id) storedIn {
cast[id] = it.cast
crew[id] = it.crew
}
}
override suspend fun getVideos(id: Int) {
movieService.getVideos(id) storedIn { videos[id] = it.results }
}
override suspend fun getReviews(id: Int) {
movieService.getReviews(id) storedIn { reviews[id] = it.results }
}
override suspend fun getKeywords(id: Int) {
movieService.getKeywords(id) storedIn { keywords[id] = it.keywords ?: emptyList() }
}
override suspend fun getWatchProviders(id: Int) {
movieService.getWatchProviders(id) storedIn {
it.results[Locale.getDefault().country]?.let { wp ->
watchProviders[id] = wp
}
}
}
override suspend fun getExternalIds(id: Int) {
movieService.getExternalIds(id) storedIn { externalIds[id] = it }
}
override suspend fun getAccountStates(id: Int) {
val sessionId = SessionManager.currentSession.value?.sessionId ?: throw Exception("Session must not be null")
val response = movieService.getAccountStates(id, sessionId)
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got account states: $it")
accountStates[id] = it
} ?: run {
Log.d(TAG, "Problem getting account states")
}
} else {
Log.d(TAG, "Issue getting account states: $response")
}
// movieService.getAccountStates(id) storedIn { accountStates[id] = it }
}
suspend fun getReleaseDates(id: Int) {
movieService.getReleaseDates(id) storedIn { releaseDates[id] = it.releaseDates }
}
override suspend fun postRating(id: Int, ratingBody: RatingBody) {
val session = SessionManager.currentSession.value ?: throw Exception("Session must not be null")
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")
}
}
override suspend fun deleteRating(id: Int) {
val session = SessionManager.currentSession.value ?: throw Exception("Session must not be null")
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")
}
}
override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> {
return movieService.getSimilarMovies(id, page)
}
override suspend fun getPopular(page: Int): Response<out HomePageResponse> {
return movieService.getPopularMovies(page)
@@ -36,55 +157,42 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
override suspend fun getUpcoming(page: Int): Response<out HomePageResponse> {
return movieService.getUpcomingMovies(page)
}
}
suspend fun getReleaseDates(id: Int): Response<MovieReleaseResults> {
return movieService.getReleaseDates(id)
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 getById(id: Int): Response<out DetailedItem> {
return movieService.getMovieById(id)
}
override suspend fun getImages(id: Int): Response<ImageCollection> {
return movieService.getMovieImages(id)
}
override suspend fun getCastAndCrew(id: Int): Response<CastAndCrew> {
return movieService.getCastAndCrew(id)
}
override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> {
return movieService.getSimilarMovies(id, page)
}
override suspend fun getVideos(id: Int): Response<VideoResponse> {
return movieService.getVideos(id)
}
override suspend fun getReviews(id: Int): Response<ReviewResponse> {
return movieService.getReviews(id)
}
override suspend fun postRating(id: Int, rating: RatingBody): Response<StatusResponse> {
val session = SessionManager.currentSession.value ?: throw Exception("Session must not be null")
return movieService.postMovieRatingAsUser(id, session.sessionId, rating)
}
override suspend fun deleteRating(id: Int): Response<StatusResponse> {
val session = SessionManager.currentSession.value ?: throw Exception("Session must not be null")
return movieService.deleteMovieReviewAsUser(id, session.sessionId)
}
override suspend fun getKeywords(id: Int): Response<KeywordsResponse> {
return movieService.getKeywords(id)
}
override suspend fun getWatchProviders(id: Int): Response<WatchProviderResponse> {
return movieService.getWatchProviders(id)
}
override suspend fun getExternalIds(id: Int): Response<ExternalIds> {
return movieService.getExternalIds(id)
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

@@ -1,36 +1,93 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3
import android.util.Log
import androidx.compose.runtime.mutableStateMapOf
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCast
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCrew
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailPerson
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ExternalIds
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePagePeopleResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.PersonCreditsResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.PersonImage
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.PersonImageCollection
import okhttp3.internal.notify
import org.koin.core.component.KoinComponent
import retrofit2.Response
import java.util.Collections
class PeopleService: KoinComponent {
private val TAG = "PeopleService"
private val service by lazy { TmdbClient().createPeopleService() }
suspend fun getPerson(id: Int): Response<DetailPerson> {
return service.getPerson(id)
val peopleMap = Collections.synchronizedMap(mutableStateMapOf<Int, DetailPerson>())
val castMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<DetailCast>>())
val crewMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<DetailCrew>>())
val imagesMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<PersonImage>>())
val externalIdsMap = Collections.synchronizedMap(mutableStateMapOf<Int, ExternalIds>())
suspend fun getPerson(id: Int) {
val response = service.getPerson(id)
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got person $id")
peopleMap[id] = it
} ?: run {
Log.w(TAG, "Problem getting person $id")
}
} else {
Log.e(TAG, "Issue getting person $id")
}
}
suspend fun getCredits(id: Int): Response<PersonCreditsResponse> {
return service.getCredits(id)
suspend fun getCredits(id: Int) {
val response = service.getCredits(id)
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got credits $id")
castMap[id] = it.cast
crewMap[id] = it.crew
} ?: run {
Log.w(TAG, "Problem getting credits $id")
}
} else {
Log.e(TAG, "Issue getting credits $id")
}
}
suspend fun getImages(id: Int): Response<PersonImageCollection> {
return service.getImages(id)
suspend fun getImages(id: Int) {
val response = service.getImages(id)
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got images $id")
imagesMap[id] = it.images
} ?: run {
Log.w(TAG, "Problem getting images $id")
}
} else {
Log.e(TAG, "Issues getting images $id")
}
}
suspend fun getPopular(page: Int = 1): Response<HomePagePeopleResponse> {
suspend fun getExternalIds(id: Int) {
val response = service.getExternalIds(id)
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got external ids $id")
externalIdsMap[id] = it
} ?: run {
Log.w(TAG, "Problem getting external ids $id")
}
} else {
Log.e(TAG, "Issue getting external ids $id")
}
}
suspend fun getPopular(page: Int): Response<HomePagePeopleResponse> {
return service.getPopular(page)
}
suspend fun getExternalIds(id: Int): Response<ExternalIds> {
return service.getExternalIds(id)
}
}

View File

@@ -1,46 +1,98 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import androidx.compose.runtime.mutableStateOf
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Collection
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Keyword
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ProductionCompany
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResult
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 kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response
import java.util.*
class SearchService: KoinComponent {
private val service: SearchApi by inject()
private val service: SearchService by inject()
suspend fun searchCompanies(query: String, page: Int = 1): Response<SearchResult<ProductionCompany>> {
val movieResults = mutableStateOf<Flow<PagingData<SearchResultMovie>>?>(null)
val tvResults = mutableStateOf<Flow<PagingData<SearchResultTv>>?>(null)
val peopleResults = mutableStateOf<Flow<PagingData<SearchResultPerson>>?>(null)
val multiResults = mutableStateOf<Flow<PagingData<SortableSearchResult>>?>(null)
fun searchCompanies(query: String, page: Int = 1): Response<SearchResult<ProductionCompany>> {
return service.searchCompanies(query, page)
}
suspend fun searchCollections(query: String, page: Int = 1): Response<SearchResult<Collection>> {
fun searchCollections(query: String, page: Int = 1): Response<SearchResult<Collection>> {
return service.searchCollections(query, page)
}
suspend fun searchKeywords(query: String, page: Int = 1): Response<SearchResult<Keyword>> {
fun searchKeywords(query: String, page: Int = 1): Response<SearchResult<Keyword>> {
return service.searchKeywords(query, page)
}
suspend fun searchMovies(query: String, page: Int = 1): Response<SearchResult<SearchResultMovie>> {
fun searchMovies(query: String, page: Int): Response<SearchResult<SearchResultMovie>> {
return service.searchMovies(query, page)
}
suspend fun searchTv(query: String, page: Int = 1): Response<SearchResult<SearchResultTv>> {
fun searchTv(query: String, page: Int): Response<SearchResult<SearchResultTv>> {
return service.searchTv(query, page)
}
suspend fun searchPeople(query: String, page: Int = 1): Response<SearchResult<SearchResultPerson>> {
fun searchPeople(query: String, page: Int): Response<SearchResult<SearchResultPerson>> {
return service.searchPeople(query, page)
}
suspend fun searchMulti(query: String, page: Int = 1): Response<SearchResult<SortableSearchResult>> {
fun searchMulti(query: String, page: Int): Response<SearchResult<SortableSearchResult>> {
return service.searchMulti(query, page)
}
}
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

@@ -64,4 +64,7 @@ interface TvApi {
@GET("tv/{id}/external_ids")
suspend fun getExternalIds(@Path("id") id: Int): Response<ExternalIds>
@GET("tv/{id}/account_states")
suspend fun getAccountStates(@Path("id") id: Int): Response<AccountStates>
}

View File

@@ -1,26 +1,148 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3
import android.util.Log
import androidx.compose.runtime.mutableStateMapOf
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.owenlejeune.tvtime.api.storedIn
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CastAndCrew
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedItem
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedTv
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ExternalIds
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePageResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Keyword
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KeywordsResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatingBody
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Review
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ReviewResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Season
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.StatusResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvContentRatings
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.VideoResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviderResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders
import com.owenlejeune.tvtime.utils.SessionManager
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response
import java.util.Collections
import java.util.Locale
class TvService: KoinComponent, DetailService, HomePageService {
private val service by lazy { TmdbClient().createTvService() }
companion object {
private const val TAG = "TvService"
}
private val service: TvApi by inject()
val detailTv = Collections.synchronizedMap(mutableStateMapOf<Int, DetailedTv>())
val images = Collections.synchronizedMap(mutableStateMapOf<Int, ImageCollection>())
val cast = Collections.synchronizedMap(mutableStateMapOf<Int, List<CastMember>>())
val crew = Collections.synchronizedMap(mutableStateMapOf<Int, List<CrewMember>>())
val videos = Collections.synchronizedMap(mutableStateMapOf<Int, List<Video>>())
val reviews = Collections.synchronizedMap(mutableStateMapOf<Int, List<Review>>())
val keywords = Collections.synchronizedMap(mutableStateMapOf<Int, List<Keyword>>())
val watchProviders = Collections.synchronizedMap(mutableStateMapOf<Int, WatchProviders>())
val externalIds = Collections.synchronizedMap(mutableStateMapOf<Int, ExternalIds>())
val contentRatings = Collections.synchronizedMap(mutableStateMapOf<Int, List<TvContentRatings.TvContentRating>>())
val similar = Collections.synchronizedMap(mutableStateMapOf<Int, Flow<PagingData<TmdbItem>>>())
val accountStates = Collections.synchronizedMap(mutableStateMapOf<Int, AccountStates>())
private val _seasons = Collections.synchronizedMap(mutableStateMapOf<Int, MutableSet<Season>>())
val seasons: MutableMap<Int, out Set<Season>>
get() = _seasons
override suspend fun getById(id: Int) {
service.getTvShowById(id) storedIn { detailTv[id] = it }
}
override suspend fun getImages(id: Int) {
service.getTvImages(id) storedIn { images[id] = it }
}
override suspend fun getCastAndCrew(id: Int) {
service.getCastAndCrew(id) storedIn {
cast[id] = it.cast
crew[id] = it.crew
}
}
suspend fun getContentRatings(id: Int) {
service.getContentRatings(id) storedIn { contentRatings[id] = it.results }
}
override suspend fun getVideos(id: Int) {
service.getVideos(id) storedIn { videos[id] = it.results }
}
override suspend fun getReviews(id: Int) {
service.getReviews(id) storedIn { reviews[id] = it.results }
}
override suspend fun getKeywords(id: Int) {
service.getKeywords(id) storedIn { keywords[id] = it.keywords ?: emptyList() }
}
override suspend fun getWatchProviders(id: Int) {
service.getWatchProviders(id) storedIn {
it.results[Locale.getDefault().country]?.let { wp ->
watchProviders[id] = wp
}
}
}
override suspend fun getAccountStates(id: Int) {
service.getAccountStates(id) storedIn { accountStates[id] = it }
}
suspend fun getSeason(seriesId: Int, seasonId: Int) {
service.getSeason(seriesId, seasonId) storedIn {
_seasons[seriesId]?.add(it) ?: run {
_seasons[seriesId] = mutableSetOf(it)
}
}
}
override suspend fun getExternalIds(id: Int) {
service.getExternalIds(id) storedIn { externalIds[id] = it }
}
override suspend fun postRating(id: Int, ratingBody: RatingBody) {
val session = SessionManager.currentSession.value ?: throw Exception("Session must not be null")
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")
}
}
override suspend fun deleteRating(id: Int) {
val session = SessionManager.currentSession.value ?: throw Exception("Session must not be null")
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)
}
override suspend fun getPopular(page: Int): Response<out HomePageResponse> {
return service.getPoplarTv(page)
@@ -38,58 +160,42 @@ class TvService: KoinComponent, DetailService, HomePageService {
return service.getTvOnTheAir(page)
}
override suspend fun getById(id: Int): Response<out DetailedItem> {
return service.getTvShowById(id)
}
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 getImages(id: Int): Response<ImageCollection> {
return service.getTvImages(id)
}
override suspend fun getCastAndCrew(id: Int): Response<CastAndCrew> {
return service.getCastAndCrew(id)
}
suspend fun getContentRatings(id: Int): Response<TvContentRatings> {
return service.getContentRatings(id)
}
override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> {
return service.getSimilarTvShows(id, page)
}
override suspend fun getVideos(id: Int): Response<VideoResponse> {
return service.getVideos(id)
}
override suspend fun getReviews(id: Int): Response<ReviewResponse> {
return service.getReviews(id)
}
override suspend fun postRating(id: Int, ratingBody: RatingBody): Response<StatusResponse> {
val session = SessionManager.currentSession.value ?: throw Exception("Session must not be null")
return service.postTvRatingAsUser(id, session.sessionId, ratingBody)
}
override suspend fun deleteRating(id: Int): Response<StatusResponse> {
val session = SessionManager.currentSession.value ?: throw Exception("Session must not be null")
return service.deleteTvReviewAsUser(id, session.sessionId)
}
override suspend fun getKeywords(id: Int): Response<KeywordsResponse> {
return service.getKeywords(id)
}
override suspend fun getWatchProviders(id: Int): Response<WatchProviderResponse> {
return service.getWatchProviders(id)
}
suspend fun getSeason(seriesId: Int, seasonId: Int): Response<Season> {
return service.getSeason(seriesId, seasonId)
}
override suspend fun getExternalIds(id: Int): Response<ExternalIds> {
return service.getExternalIds(id)
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

@@ -0,0 +1,22 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer
import com.google.gson.JsonObject
import com.owenlejeune.tvtime.api.tmdb.api.BaseDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates
class AccountStatesDeserializer: BaseDeserializer<AccountStates>() {
override fun processJson(obj: JsonObject): AccountStates {
val id = obj.get("id").asInt
val isFavorite = obj.get("favorite").asBoolean
val isWatchlist = obj.get("watchlist").asBoolean
return try {
val isRated = obj.get("rated").asBoolean
AccountStates(id, isFavorite, isWatchlist, isRated, -1)
} catch (e: Exception) {
val rating = obj.get("rated").asJsonObject.get("value").asInt
AccountStates(id, isFavorite, isWatchlist, true, rating)
}
}
}

View File

@@ -0,0 +1,14 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
class AccountStates(
@SerializedName("id")
val id: Int,
@SerializedName("favorite")
val isFavorite: Boolean,
@SerializedName("watchlist")
val isWatchListed: Boolean,
val isRated: Boolean,
val rating: Int
)

View File

@@ -1,9 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
class GuestSessionResponse(
@SerializedName("success") val success: Boolean,
@SerializedName("guest_session_id") val guestSessionId: String,
@SerializedName("expires_at") val expiry: String
)

View File

@@ -12,8 +12,7 @@ import org.koin.core.component.inject
class HomePagePeoplePagingSource: PagingSource<Int, HomePagePerson>(), KoinComponent {
private val service: PeopleApi by inject()
private val context: Context by inject()
private val service: PeopleService by inject()
override fun getRefreshKey(state: PagingState<Int, HomePagePerson>): Int? {
return state.anchorPosition
@@ -32,7 +31,6 @@ class HomePagePeoplePagingSource: PagingSource<Int, HomePagePerson>(), KoinCompo
nextKey = if (results.isEmpty() || responseBody == null) null else responseBody.page + 1
)
} else {
// Toast.makeText(context, "No more results found", Toast.LENGTH_SHORT).show()
LoadResult.Invalid()
}
} catch (e: Exception) {

View File

@@ -7,11 +7,7 @@ import retrofit2.http.*
interface ListV4Api {
@GET("list/{id}")
suspend fun getList(
@Path("id") listId: Int,
@Query("api_key") apiKey: String,
@Query("page") page: Int = 1
): Response<MediaList>
suspend fun getList(@Path("id") listId: Int): Response<MediaList>
@POST("list")
suspend fun createList(@Body body: CreateListBody): Response<CreateListResponse>

View File

@@ -1,52 +1,74 @@
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 org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response
class ListV4Service: KoinComponent {
private val service by lazy { TmdbClient().createV4ListService() }
private val preferences: AppPreferences by inject()
suspend fun getList(listId: Int, page: Int = 1): Response<MediaList> {
return service.getList(listId, BuildConfig.TMDB_Api_v4Key, page)
companion object {
private const val TAG = "ListV4Service"
}
suspend fun createList(body: CreateListBody): Response<CreateListResponse> {
return service.createList(body)
private val service: ListV4Api by inject()
val listMap = mutableStateMapOf<Int, MediaList>()
suspend fun getList(listId: Int) {
val response = service.getList(listId)
if (response.isSuccessful) {
response.body()?.let {
listMap[listId] = it
}
}
}
suspend fun updateList(listId: Int, body: ListUpdateBody): Response<StatusResponse> {
return service.updateList(listId, body)
suspend fun createList(body: CreateListBody) {//}: Response<CreateListResponse> {
service.createList(body)
}
suspend fun clearList(listId: Int): Response<ClearListResponse> {
return service.clearList(listId)
suspend fun updateList(listId: Int, body: ListUpdateBody) {
val response = service.updateList(listId, body)
if (response.isSuccessful) {
Log.d(TAG, "Successfully updated list $listId")
getList(listId)
} else {
Log.w(TAG, "Issue updating list $listId")
}
}
suspend fun deleteList(listId: Int): Response<StatusResponse> {
return service.deleteList(listId)
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)
}
}
suspend fun addItemsToList(listId: Int, body: AddToListBody): Response<AddToListResponse> {
return service.addItemsToList(listId, body)
suspend fun clearList(listId: Int) {//}: Response<ClearListResponse> {
service.clearList(listId)
}
suspend fun updateListItems(listId: Int, body: UpdateListItemBody): Response<AddToListResponse> {
return service.updateListItems(listId, body)
suspend fun deleteList(listId: Int) {//}: Response<StatusResponse> {
service.deleteList(listId)
}
suspend fun deleteListItems(listId: Int, body: DeleteListItemsBody): Response<AddToListResponse> {
return service.deleteListItems(listId, body)
suspend fun addItemsToList(listId: Int, body: AddToListBody) {//}: Response<AddToListResponse> {
service.addItemsToList(listId, body)
}
suspend fun getListItemStatus(listId: Int, mediaId: Int, mediaType: String): Response<ListItemStatusResponse> {
return service.getListItemStatus(listId, mediaId, mediaType)
suspend fun updateListItems(listId: Int, body: UpdateListItemBody) {//}: Response<AddToListResponse> {
service.updateListItems(listId, body)
}
suspend fun getListItemStatus(listId: Int, mediaId: Int, mediaType: String) {//}: Response<ListItemStatusResponse> {
service.getListItemStatus(listId, mediaId, mediaType)
}
}

View File

@@ -1,38 +0,0 @@
//package com.owenlejeune.tvtime.api.tmdb.paging
//
//import androidx.paging.PagingSource
//import androidx.paging.PagingState
//import com.owenlejeune.tvtime.api.tmdb.TmdbClient
//import com.owenlejeune.tvtime.api.tmdb.api.v3.model.PopularMovie
//import retrofit2.HttpException
//import java.io.IOException
//
//class PopularMovieSource: PagingSource<Int, PopularMovie>() {
//
// companion object {
// const val MIN_PAGE = 1
// const val MAX_PAGE = 1000
// }
//
// private val movieService by lazy { TmdbClient().createMovieService() }
//
// override fun getRefreshKey(state: PagingState<Int, PopularMovie>): Int? {
// return state.anchorPosition
// }
//
// override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PopularMovie> {
// return try {
// val nextPage = params.key ?: 1
// val movieList = movieService.getPopularMovies(page = nextPage)
// LoadResult.Page(
// data = movieList.movies,
// prevKey = if (nextPage == MIN_PAGE) null else nextPage - 1,
// nextKey = if (movieList.count == 0 || nextPage > MAX_PAGE) null else movieList.page + 1
// )
// } catch (exception: IOException) {
// return LoadResult.Error(exception)
// } catch (exception: HttpException) {
// return LoadResult.Error(exception)
// }
// }
//}

View File

@@ -6,11 +6,21 @@ import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.api.*
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.ConfigurationService
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.SearchService
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.AccountStatesDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.KnownForDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.SortableSearchResultDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KnownFor
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult
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.ListV4Service
import com.owenlejeune.tvtime.api.tmdb.api.v4.deserializer.ListItemDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListItem
import com.owenlejeune.tvtime.preferences.AppPreferences
@@ -30,7 +40,6 @@ val networkModule = module {
single { get<TmdbClient>().createV4AccountService() }
single { get<TmdbClient>().createV4ListService() }
single { get<TmdbClient>().createAccountService() }
single { get<TmdbClient>().createGuestSessionService() }
single { get<TmdbClient>().createAuthenticationService() }
single { get<TmdbClient>().createMovieService() }
single { get<TmdbClient>().createPeopleService() }
@@ -38,14 +47,23 @@ val networkModule = module {
single { get<TmdbClient>().createTvService() }
single { get<TmdbClient>().createConfigurationService() }
single { ConfigurationService() }
single { MoviesService() }
single { TvService() }
single { AccountService() }
single { AuthenticationService() }
single { PeopleService() }
single { SearchService() }
single { AccountV4Service() }
single { AuthenticationV4Service() }
single { ListV4Service() }
single<Map<Class<*>, JsonDeserializer<*>>> {
mapOf(
ListItem::class.java to ListItemDeserializer(),
KnownFor::class.java to KnownForDeserializer(),
SortableSearchResult::class.java to SortableSearchResultDeserializer()
SortableSearchResult::class.java to SortableSearchResultDeserializer(),
AccountStates::class.java to AccountStatesDeserializer()
)
}

View File

@@ -0,0 +1,9 @@
package com.owenlejeune.tvtime.extensions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
fun Any.coroutineTask(runnable: suspend () -> Unit) {
CoroutineScope(Dispatchers.IO).launch { runnable() }
}

View File

@@ -0,0 +1,258 @@
package com.owenlejeune.tvtime.ui.components
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.ui.theme.FavoriteSelected
import com.owenlejeune.tvtime.ui.theme.RatingSelected
import com.owenlejeune.tvtime.ui.theme.WatchlistSelected
import com.owenlejeune.tvtime.ui.theme.actionButtonColor
import com.owenlejeune.tvtime.ui.viewmodel.AccountViewModel
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.launch
import java.text.DecimalFormat
enum class Actions {
RATE,
WATCHLIST,
LIST,
FAVORITE
}
@Composable
fun ActionsView(
itemId: Int,
type: MediaViewType,
actions: List<Actions> = listOf(Actions.RATE, Actions.WATCHLIST, Actions.LIST, Actions.FAVORITE),
modifier: Modifier = Modifier
) {
val accountViewModel = viewModel<AccountViewModel>()
val mainViewModel = viewModel<MainViewModel>()
LaunchedEffect(Unit) {
mainViewModel.getAccountStates(itemId, type)
}
Row(
modifier = modifier
.wrapContentSize()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (actions.contains(Actions.RATE)) {
RateButton(
itemId = itemId,
type = type,
mainViewModel = mainViewModel
)
}
if (actions.contains(Actions.WATCHLIST)) {
WatchlistButton(
itemId = itemId,
type = type,
accountViewModel = accountViewModel,
mainViewModel = mainViewModel
)
}
if (actions.contains(Actions.FAVORITE)) {
FavoriteButton(
itemId = itemId,
type = type,
accountViewModel = accountViewModel,
mainViewModel = mainViewModel
)
}
if (actions.contains(Actions.LIST)) {
ListButton(
itemId = itemId,
type = type
)
}
}
}
@Composable
fun ActionButton(
imageVector: ImageVector,
contentDescription: String,
isSelected: Boolean,
filledIconColor: Color,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val bgColor = MaterialTheme.colorScheme.background
val tintColor = remember { Animatable(bgColor) }
LaunchedEffect(isSelected) {
val target = if (isSelected) filledIconColor else bgColor
tintColor.animateTo(targetValue = target, animationSpec = tween(300))
}
Box(
modifier = modifier
.clip(CircleShape)
.height(40.dp)
.requiredWidthIn(min = 40.dp)
.background(color = MaterialTheme.colorScheme.actionButtonColor)
.clickable(onClick = onClick)
) {
Icon(
modifier = Modifier
.clip(CircleShape)
.align(Alignment.Center),
imageVector = imageVector,
contentDescription = contentDescription,
tint = tintColor.value
)
}
}
@Composable
private fun RateButton(
itemId: Int,
type: MediaViewType,
mainViewModel: MainViewModel,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
val accountStates = remember { mainViewModel.produceAccountStatesFor(type) }
val itemIsRated = accountStates[itemId]?.isRated ?: false
val showRatingDialog = remember { mutableStateOf(false) }
ActionButton(
imageVector = Icons.Filled.Star,
contentDescription = "",
isSelected = itemIsRated,
filledIconColor = RatingSelected,
onClick = { showRatingDialog.value = true },
modifier = modifier
)
val userRating = accountStates[itemId]?.rating?.times(2)?.toFloat() ?: 0f
RatingDialog(
showDialog = showRatingDialog,
rating = userRating,
onValueConfirmed = { rating ->
if (rating > 0f) {
scope.launch { mainViewModel.postRating(itemId, rating, type) }
} else {
scope.launch { mainViewModel.deleteRating(itemId, type) }
}
}
)
}
@Composable
fun WatchlistButton(
itemId: Int,
type: MediaViewType,
accountViewModel: AccountViewModel,
mainViewModel: MainViewModel,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
val accountStates = remember { mainViewModel.produceAccountStatesFor(type) }
val itemIsWatchlisted = accountStates[itemId]?.isWatchListed ?: false
ActionButton(
modifier = modifier,
imageVector = Icons.Filled.Bookmark,
contentDescription = "",
isSelected = itemIsWatchlisted,
filledIconColor = WatchlistSelected,
onClick = {
scope.launch {
accountViewModel.addToWatchlist(type, itemId, !itemIsWatchlisted)
mainViewModel.getAccountStates(itemId, type)
}
}
)
}
@Composable
fun ListButton(
itemId: Int,
type: MediaViewType,
modifier: Modifier = Modifier
) {
CircleBackgroundColorImage(
modifier = modifier.clickable(
onClick = {}
),
size = 40.dp,
backgroundColor = MaterialTheme.colorScheme.actionButtonColor,
image = Icons.Filled.List,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.background),
contentDescription = ""
)
}
@Composable
fun FavoriteButton(
itemId: Int,
type: MediaViewType,
accountViewModel: AccountViewModel,
mainViewModel: MainViewModel,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
val accountStates = remember { mainViewModel.produceAccountStatesFor(type) }
val itemIsFavorited = accountStates[itemId]?.isFavorite ?: false
ActionButton(
modifier = modifier,
imageVector = Icons.Filled.Favorite,
contentDescription = "",
isSelected = itemIsFavorited,
filledIconColor = FavoriteSelected,
onClick = {
scope.launch {
accountViewModel.addToFavourites(type, itemId, !itemIsFavorited)
mainViewModel.getAccountStates(itemId, type)
}
}
)
}

View File

@@ -0,0 +1,77 @@
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
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.owenlejeune.tvtime.R
import java.text.DecimalFormat
@Composable
fun RatingDialog(
showDialog: MutableState<Boolean>,
rating: Float,
onValueConfirmed: (Float) -> Unit
) {
val formatPosition: (Float) -> String = { position ->
DecimalFormat("#.#").format(position.toInt()*5/10f)
}
if (showDialog.value) {
var sliderPosition by remember { mutableStateOf(rating) }
val formatted = formatPosition(sliderPosition).toFloat()
AlertDialog(
modifier = Modifier.wrapContentHeight(),
onDismissRequest = { showDialog.value = false },
title = { Text(text = stringResource(R.string.rating_dialog_title)) },
confirmButton = {
Button(
modifier = Modifier.height(40.dp),
onClick = {
onValueConfirmed.invoke(formatted)
showDialog.value = false
}
) {
Text(
text = if (formatted > 0f) {
stringResource(id = R.string.rating_dialog_confirm)
} else {
stringResource(id = R.string.rating_dialog_delete)
}
)
}
},
dismissButton = {
Button(
modifier = Modifier.height(40.dp),
onClick = {
showDialog.value = false
}
) {
Text(stringResource(R.string.action_cancel))
}
},
text = {
SliderWithLabel(
value = sliderPosition,
valueRange = 0f..20f,
onValueChanged = {
sliderPosition = it
},
sliderLabel = "${sliderPosition.toInt() * 5}%"
)
}
)
}
}

View File

@@ -19,10 +19,11 @@ fun SliderWithLabel(
valueRange: ClosedFloatingPointRange<Float>,
onValueChanged: (Float) -> Unit,
sliderLabel: String,
modifier: Modifier = Modifier,
steps: Int = 0,
labelMinWidth: Dp = 36.dp
labelMinWidth: Dp = 46.dp
) {
Column {
Column(modifier = modifier) {
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
@@ -60,7 +61,7 @@ fun SliderWithLabel(
@Composable
fun SliderLabel(label: String, minWidth: Dp, modifier: Modifier = Modifier) {
Text(
label,
text = label,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onPrimary,
modifier = modifier

View File

@@ -36,9 +36,6 @@ sealed class AccountTabNavItem(
val noContentText = resourceUtils.getString(noContentStringRes)
companion object {
val GuestItems
get() = listOf(RatedMovies, RatedTvShows, RatedTvEpisodes)
val AuthorizedItems
get() = listOf(
RatedMovies, RatedTvShows, RatedTvEpisodes, FavoriteMovies, FavoriteTvShows,

View File

@@ -1,4 +1,5 @@
package com.owenlejeune.tvtime.ui.screens
import android.accounts.Account
import android.content.Context
import android.content.Intent
import android.util.Log
@@ -35,6 +36,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -47,11 +49,15 @@ import com.owenlejeune.tvtime.api.tmdb.api.v4.model.*
import com.owenlejeune.tvtime.extensions.WindowSizeClass
import com.owenlejeune.tvtime.extensions.unlessEmpty
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.Actions
import com.owenlejeune.tvtime.ui.components.ActionsView
import com.owenlejeune.tvtime.ui.components.FavoriteButton
import com.owenlejeune.tvtime.ui.components.RatingView
import com.owenlejeune.tvtime.ui.components.Spinner
import com.owenlejeune.tvtime.ui.components.SwitchPreference
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.theme.*
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
@@ -68,22 +74,22 @@ import kotlin.math.roundToInt
@Composable
fun ListDetailScreen(
appNavController: NavController,
itemId: Int?,
itemId: Int,
windowSize: WindowSizeClass,
preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java)
service: ListV4Service = KoinJavaComponent.get(ListV4Service::class.java)
) {
val accountViewModel = viewModel<AccountViewModel>()
LaunchedEffect(Unit) {
accountViewModel.getList(itemId)
}
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(color = MaterialTheme.colorScheme.background)
systemUiController.setNavigationBarColor(color = MaterialTheme.colorScheme.background)
val service = ListV4Service()
val listMap = remember { accountViewModel.listMap }
val parentList = listMap[itemId]
val parentList = remember { mutableStateOf<MediaList?>(null) }
itemId?.let {
if (parentList.value == null) {
fetchList(itemId, service, parentList)
}
}
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val topAppBarScrollState = rememberTopAppBarScrollState()
@@ -101,7 +107,7 @@ fun ListDetailScreen(
scrolledContainerColor = MaterialTheme.colorScheme.background,
titleContentColor = MaterialTheme.colorScheme.primary
),
title = { Text(text = parentList.value?.name ?: "") },
title = { Text(text = parentList?.name ?: "") },
navigationIcon = {
IconButton(
onClick = { appNavController.popBackStack() }
@@ -117,7 +123,7 @@ fun ListDetailScreen(
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
parentList.value?.let { mediaList ->
parentList?.let { mediaList ->
Column(
modifier = Modifier
.padding(all = 12.dp)
@@ -151,7 +157,7 @@ private fun ListHeader(
list: MediaList,
selectedSortOrder: MutableState<SortOrder>,
service: ListV4Service,
parentList: MutableState<MediaList?>
parentList: MediaList?
) {
val context = LocalContext.current
@@ -254,10 +260,7 @@ private fun ListHeader(
if (showEditListDialog.value) {
EditListDialog(
showEditListDialog = showEditListDialog,
list = list,
service = service,
parentList = parentList,
selectedSortOrder = selectedSortOrder
list = list
)
}
}
@@ -312,11 +315,10 @@ private fun SortOrderDialog(
@Composable
private fun EditListDialog(
showEditListDialog: MutableState<Boolean>,
list: MediaList,
service: ListV4Service,
parentList: MutableState<MediaList?>,
selectedSortOrder: MutableState<SortOrder>
list: MediaList
) {
val accountViewModel = viewModel<AccountViewModel>()
val coroutineScope = rememberCoroutineScope()
var listTitle by remember { mutableStateOf(list.name) }
@@ -338,11 +340,7 @@ private fun EditListDialog(
onClick = {
val listUpdateBody = ListUpdateBody(listTitle, listDescription, isPublicList, editSelectedSortOrder)
coroutineScope.launch {
val response = service.updateList(list.id, listUpdateBody)
if (response.isSuccessful) {
fetchList(list.id, service, parentList)
selectedSortOrder.value = editSelectedSortOrder
}
accountViewModel.updateList(list.id, listUpdateBody)
showEditListDialog.value = false
}
}
@@ -426,23 +424,24 @@ private fun RowScope.OverviewStatCard(
private fun ListItemView(
appNavController: NavController,
listItem: ListItem,
list: MutableState<MediaList?>
list: MediaList?
) {
val context = LocalContext.current
val accountViewModel = viewModel<AccountViewModel>()
val scope = rememberCoroutineScope()
RevealSwipe (
directions = setOf(RevealDirection.EndToStart),
hiddenContentEnd = {
IconButton(
modifier = Modifier.padding(horizontal = 15.dp),
onClick = {
removeItemFromList(
context = context,
itemId = listItem.id,
itemType = listItem.mediaType,
itemName = listItem.title,
service = ListV4Service(),
list = list
)
scope.launch {
accountViewModel.deleteListItem(
list?.id ?: -1,
listItem.id,
listItem.mediaType
)
}
}
) {
Icon(
@@ -533,7 +532,11 @@ private fun ListItemView(
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
ActionButtonRow(listItem)
ActionsView(
itemId = listItem.id,
type = listItem.mediaType,
actions = listOf(Actions.RATE, Actions.WATCHLIST, Actions.FAVORITE)
)
Spacer(modifier = Modifier.weight(1f))
}
@@ -553,109 +556,6 @@ private fun ListItemView(
}
}
@Composable
private fun ActionButtonRow(listItem: ListItem) {
val session = SessionManager.currentSession.value
val (isFavourited, isWatchlisted, isRated) = if (listItem.mediaType == MediaViewType.MOVIE) {
Triple(
session?.hasFavoritedMovie(listItem.id) == true,
session?.hasWatchlistedMovie(listItem.id) == true,
session?.hasRatedMovie(listItem.id) == true
)
} else {
Triple(
session?.hasFavoritedTvShow(listItem.id) == true,
session?.hasWatchlistedTvShow(listItem.id) == true,
session?.hasRatedTvShow(listItem.id) == true
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
ActionButton(
itemId = listItem.id,
type = listItem.mediaType,
imageVector = Icons.Filled.Favorite,
contentDescription = stringResource(id = R.string.favourite_label),
isSelected = isFavourited,
filledIconColor = FavoriteSelected,
onClick = ::listAddToFavorite
)
ActionButton(
itemId = listItem.id,
type = listItem.mediaType,
imageVector = Icons.Filled.Bookmark,
contentDescription = "",
isSelected = isWatchlisted,
filledIconColor = WatchlistSelected,
onClick = ::listAddToWatchlist
)
val context = LocalContext.current
ActionButton(
itemId = listItem.id,
type = listItem.mediaType,
imageVector = Icons.Filled.Star,
contentDescription = "",
isSelected = isRated,
filledIconColor = RatingSelected,
onClick = { c, i, t, s, f ->
// todo - add rating
Toast.makeText(context, "Rating", Toast.LENGTH_SHORT).show()
}
)
}
}
private fun listAddToWatchlist(
context: Context,
itemId: Int,
type: MediaViewType,
itemIsWatchlisted: MutableState<Boolean>,
onWatchlistChanged: (Boolean) -> Unit
) {
val currentSession = SessionManager.currentSession.value
val accountId = currentSession!!.accountDetails.value!!.id
CoroutineScope(Dispatchers.IO).launch {
val response = AccountService().addToWatchlist(accountId, WatchlistBody(type, itemId, !itemIsWatchlisted.value))
if (response.isSuccessful) {
currentSession.refresh(changed = SessionManager.Session.Changed.Watchlist)
withContext(Dispatchers.Main) {
itemIsWatchlisted.value = !itemIsWatchlisted.value
onWatchlistChanged(itemIsWatchlisted.value)
}
} else {
withContext(Dispatchers.Main) {
Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun listAddToFavorite(
context: Context,
itemId: Int,
type: MediaViewType,
itemIsFavorited: MutableState<Boolean>,
onFavoriteChanged: (Boolean) -> Unit
) {
val currentSession = SessionManager.currentSession.value
val accountId = currentSession!!.accountDetails.value!!.id
CoroutineScope(Dispatchers.IO).launch {
val response = AccountService().markAsFavorite(accountId, MarkAsFavoriteBody(type, itemId, !itemIsFavorited.value))
if (response.isSuccessful) {
currentSession.refresh(changed = SessionManager.Session.Changed.Favorites)
withContext(Dispatchers.Main) {
itemIsFavorited.value = !itemIsFavorited.value
onFavoriteChanged(itemIsFavorited.value)
}
}
}
}
private fun shareListUrl(context: Context, listId: Int) {
val shareUrl = "https://www.themoviedb.org/list/$listId"
val sendIntent = Intent().apply {
@@ -665,46 +565,4 @@ private fun shareListUrl(context: Context, listId: Int) {
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
}
private fun fetchList(
itemId: Int,
service: ListV4Service,
listItem: MutableState<MediaList?>
) {
CoroutineScope(Dispatchers.IO).launch {
val response = service.getList(itemId)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
listItem.value = response.body()
}
}
}
}
private fun removeItemFromList(
context: Context,
itemName: String,
itemId: Int,
itemType: MediaViewType,
service: ListV4Service,
list: MutableState<MediaList?>
) {
CoroutineScope(Dispatchers.IO).launch {
val listId = list.value?.id ?: 0
val removeItem = DeleteListItemsItem(itemId, itemType)
val result = service.deleteListItems(listId, DeleteListItemsBody(listOf(removeItem)))
if (result.isSuccessful) {
SessionManager.currentSession.value?.refresh(SessionManager.Session.Changed.List)
service.getList(listId).body()?.let {
withContext(Dispatchers.Main) {
list.value = it
}
}
Toast.makeText(context, "Successfully removed $itemName", Toast.LENGTH_SHORT).show()
} else {
Log.w("RemoveListItemError", result.toString())
Toast.makeText(context, "An error occurred!", Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -2,17 +2,29 @@ package com.owenlejeune.tvtime.ui.screens
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
@@ -20,43 +32,40 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleService
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailPerson
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ExternalIds
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.PersonCreditsResponse
import com.owenlejeune.tvtime.ui.components.ContentCard
import com.owenlejeune.tvtime.ui.components.DetailHeader
import com.owenlejeune.tvtime.ui.components.ExpandableContentCard
import com.owenlejeune.tvtime.ui.components.ExternalIdsArea
import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
@Composable
fun PersonDetailScreen(
appNavController: NavController,
personId: Int?
personId: Int
) {
val mainViewModel = viewModel<MainViewModel>()
LaunchedEffect(Unit) {
mainViewModel.getById(personId, MediaViewType.PERSON)
mainViewModel.getExternalIds(personId, MediaViewType.PERSON)
}
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(color = MaterialTheme.colorScheme.background)
systemUiController.setNavigationBarColor(color = MaterialTheme.colorScheme.background)
val person = remember { mutableStateOf<DetailPerson?>(null) }
personId?.let {
if (person.value == null) {
fetchPerson(personId, person)
}
}
val peopleMap = remember { mainViewModel.peopleMap }
val person = peopleMap[personId]
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val topAppBarScrollState = rememberTopAppBarScrollState()
@@ -74,7 +83,7 @@ fun PersonDetailScreen(
scrolledContainerColor = MaterialTheme.colorScheme.background,
titleContentColor = MaterialTheme.colorScheme.primary
),
title = { Text(text = person.value?.name ?: "") },
title = { Text(text = person?.name ?: "") },
navigationIcon = {
IconButton(onClick = { appNavController.popBackStack() }) {
Icon(
@@ -98,34 +107,23 @@ fun PersonDetailScreen(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
DetailHeader(
posterUrl = TmdbUtils.getFullPersonImagePath(person.value?.profilePath),
posterContentDescription = person.value?.profilePath
posterUrl = TmdbUtils.getFullPersonImagePath(person?.profilePath),
posterContentDescription = person?.profilePath
)
BiographyCard(person = person.value)
BiographyCard(person = person)
val externalIds = remember { mutableStateOf<ExternalIds?>(null) }
LaunchedEffect(Unit) {
scope.launch {
val response = PeopleService().getExternalIds(personId!!)
if (response.isSuccessful) {
externalIds.value = response.body()!!
}
}
}
externalIds.value?.let {
val externalIdsMap = remember { mainViewModel.peopleExternalIdsMap }
val externalIds = externalIdsMap[personId]
externalIds?.let {
ExternalIdsArea(
externalIds = it,
modifier = Modifier.padding(start = 4.dp)
)
}
val credits = remember { mutableStateOf<PersonCreditsResponse?>(null) }
personId?.let {
if (credits.value == null) {
fetchCredits(personId, credits)
}
}
val creditsMap = remember { mainViewModel.peopleCastMap }
val credits = creditsMap[personId]
ContentCard(
title = stringResource(R.string.known_for_label)
@@ -137,8 +135,8 @@ fun PersonDetailScreen(
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
items(credits.value?.cast?.size ?: 0) { i ->
val content = credits.value!!.cast[i]
items(credits?.size ?: 0) { i ->
val content = credits!![i]
TwoLineImageTextCard(
title = content.name,
@@ -149,18 +147,18 @@ fun PersonDetailScreen(
.wrapContentHeight(),
imageUrl = TmdbUtils.getFullPosterPath(content.posterPath),
onItemClicked = {
personId?.let {
appNavController.navigate(
AppNavItem.DetailView.withArgs(content.mediaType, content.id)
)
}
appNavController.navigate(
AppNavItem.DetailView.withArgs(content.mediaType, content.id)
)
}
)
}
}
}
val departments = credits.value?.crew?.map { it.department }?.toSet() ?: emptySet()
val crewMap = remember { mainViewModel.peopleCrewMap }
val crewCredits = crewMap[personId]
val departments = crewCredits?.map { it.department }?.toSet() ?: emptySet()
if (departments.isNotEmpty()) {
ContentCard(title = stringResource(R.string.also_known_for_label)) {
Column(
@@ -178,8 +176,7 @@ fun PersonDetailScreen(
.wrapContentHeight(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
val jobsInDepartment =
credits.value!!.crew.filter { it.department == department }
val jobsInDepartment = crewCredits!!.filter { it.department == department }
items(jobsInDepartment.size) { i ->
val content = jobsInDepartment[i]
val title = if (content.mediaType == MediaViewType.MOVIE) {
@@ -195,11 +192,9 @@ fun PersonDetailScreen(
.wrapContentHeight(),
imageUrl = TmdbUtils.getFullPosterPath(content.posterPath),
onItemClicked = {
personId?.let {
appNavController.navigate(
AppNavItem.DetailView.withArgs(content.mediaType, content.id)
)
}
appNavController.navigate(
AppNavItem.DetailView.withArgs(content.mediaType, content.id)
)
}
)
}
@@ -228,26 +223,4 @@ private fun BiographyCard(person: DetailPerson?) {
overflow = TextOverflow.Ellipsis
)
}
}
private fun fetchPerson(id: Int, person: MutableState<DetailPerson?>) {
CoroutineScope(Dispatchers.IO).launch {
val result = PeopleService().getPerson(id)
if (result.isSuccessful) {
withContext(Dispatchers.Main) {
person.value = result.body()
}
}
}
}
private fun fetchCredits(id: Int, credits: MutableState<PersonCreditsResponse?>) {
CoroutineScope(Dispatchers.IO).launch {
val result = PeopleService().getCredits(id)
if (result.isSuccessful) {
withContext(Dispatchers.Main) {
credits.value = result.body()
}
}
}
}

View File

@@ -17,22 +17,27 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.collectAsLazyPagingItems
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.DetailService
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
import com.owenlejeune.tvtime.api.tmdb.api.v3.SearchService
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.ui.components.MediaResultCard
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.ui.viewmodel.SearchViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent.get
import com.owenlejeune.tvtime.extensions.lazyPagingItems
@Composable
fun SearchScreen(
@@ -40,6 +45,8 @@ fun SearchScreen(
title: String,
mediaViewType: MediaViewType
) {
val searchViewModel = viewModel<SearchViewModel>()
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(color = MaterialTheme.colorScheme.background)
systemUiController.setNavigationBarColor(color = MaterialTheme.colorScheme.background)
@@ -52,6 +59,14 @@ fun SearchScreen(
val searchValue = rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
LaunchedEffect(searchValue.value) {
if (searchValue.value.isEmpty()) {
searchViewModel.resetResults()
} else {
searchViewModel.searchFor(searchValue.value, mediaViewType)
}
}
SmallTopAppBar(
title = {
TextField(
@@ -100,68 +115,20 @@ fun SearchScreen(
)
}
if (searchValue.value.isNotEmpty()) {
when (mediaViewType) {
MediaViewType.TV -> {
SearchResultListView(
showLoadingAnimation = showLoadingAnimation,
currentQuery = searchValue,
searchExecutor = { searchResults: MutableState<List<SearchResultTv>> ->
searchTv(searchValue.value, searchResults)
}
) { tv ->
TvSearchResultView(result = tv, appNavController = appNavController)
}
}
MediaViewType.MOVIE -> {
SearchResultListView(
showLoadingAnimation = showLoadingAnimation,
currentQuery = searchValue,
searchExecutor = { searchResults: MutableState<List<SearchResultMovie>> ->
searchMovies(searchValue.value, searchResults)
}
) { movie ->
MovieSearchResultView(result = movie, appNavController = appNavController)
}
}
MediaViewType.PERSON -> {
SearchResultListView(
showLoadingAnimation = showLoadingAnimation,
currentQuery = searchValue,
searchExecutor = { searchResults: MutableState<List<SearchResultPerson>> ->
searchPeople(searchValue.value, searchResults)
}
) { person ->
PeopleSearchResultView(result = person, appNavController = appNavController)
}
}
MediaViewType.MIXED -> {
SearchResultListView(
showLoadingAnimation = showLoadingAnimation,
currentQuery = searchValue,
searchExecutor = { searchResults: MutableState<List<SortableSearchResult>> ->
searchMulti(searchValue.value, searchResults)
},
) { item ->
when (item.mediaType) {
MediaViewType.MOVIE -> MovieSearchResultView(
appNavController = appNavController,
result = item as SearchResultMovie
)
MediaViewType.TV -> TvSearchResultView(
appNavController = appNavController,
result = item as SearchResultTv
)
MediaViewType.PERSON -> PeopleSearchResultView(
appNavController = appNavController,
result = item as SearchResultPerson
)
else -> {}
}
}
}
else -> {}
when (mediaViewType) {
MediaViewType.TV -> {
TvResultsView(appNavController = appNavController, searchViewModel = searchViewModel)
}
MediaViewType.MOVIE -> {
MovieResultsView(appNavController = appNavController, searchViewModel = searchViewModel)
}
MediaViewType.PERSON -> {
PeopleResultsView(appNavController = appNavController, searchViewModel = searchViewModel)
}
MediaViewType.MIXED -> {
MultiResultsView(appNavController = appNavController, searchViewModel = searchViewModel)
}
else -> {}
}
LaunchedEffect(key1 = "") {
@@ -171,41 +138,174 @@ fun SearchScreen(
}
@Composable
private fun <T: SortableSearchResult> SearchResultListView(
showLoadingAnimation: MutableState<Boolean>,
currentQuery: MutableState<String>,
searchExecutor: (MutableState<List<T>>) -> Unit,
viewRenderer: @Composable (T) -> Unit
private fun MovieResultsView(
appNavController: NavHostController,
searchViewModel: SearchViewModel
) {
val searchResults = remember { mutableStateOf(emptyList<T>()) }
LaunchedEffect(key1 = currentQuery.value) {
showLoadingAnimation.value = true
searchExecutor(searchResults)
showLoadingAnimation.value = false
}
if (currentQuery.value.isNotEmpty() && searchResults.value.isEmpty()) {
Column(
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.weight(1f))
Text(
text = stringResource(R.string.no_search_results),
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.CenterHorizontally),
fontSize = 18.sp
)
Spacer(modifier = Modifier.weight(1f))
val results = remember { searchViewModel.movieResults }
results.value?.let {
val pagingItems = it.collectAsLazyPagingItems()
if (pagingItems.itemCount > 0) {
LazyColumn(
modifier = Modifier.padding(all = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
lazyPagingItems(pagingItems) { item ->
item?.let {
MovieSearchResultView(
appNavController = appNavController,
result = item
)
}
}
}
} else {
Column(
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.weight(1f))
Text(
text = stringResource(R.string.no_search_results),
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.CenterHorizontally),
fontSize = 18.sp
)
Spacer(modifier = Modifier.weight(1f))
}
}
}
LazyColumn(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
val items = searchResults.value.sortedByDescending { it.popularity }
listItems(items) { item ->
viewRenderer(item)
}
@Composable
private fun TvResultsView(
appNavController: NavHostController,
searchViewModel: SearchViewModel
) {
val results = remember { searchViewModel.tvResults }
results.value?.let {
val pagingItems = it.collectAsLazyPagingItems()
if (pagingItems.itemCount > 0) {
LazyColumn(
modifier = Modifier.padding(all = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
lazyPagingItems(pagingItems) { item ->
item?.let {
TvSearchResultView(
appNavController = appNavController,
result = item
)
}
}
}
} else {
Column(
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.weight(1f))
Text(
text = stringResource(R.string.no_search_results),
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.CenterHorizontally),
fontSize = 18.sp
)
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
@Composable
private fun PeopleResultsView(
appNavController: NavHostController,
searchViewModel: SearchViewModel
) {
val results = remember { searchViewModel.peopleResults }
results.value?.let {
val pagingItems = it.collectAsLazyPagingItems()
if (pagingItems.itemCount > 0) {
LazyColumn(
modifier = Modifier.padding(all = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
lazyPagingItems(pagingItems) { item ->
item?.let {
PeopleSearchResultView(
appNavController = appNavController,
result = item
)
}
}
}
} else {
Column(
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.weight(1f))
Text(
text = stringResource(R.string.no_search_results),
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.CenterHorizontally),
fontSize = 18.sp
)
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
@Composable
private fun MultiResultsView(
appNavController: NavHostController,
searchViewModel: SearchViewModel
) {
val results = remember { searchViewModel.multiResults }
results.value?.let {
val pagingItems = it.collectAsLazyPagingItems()
if (pagingItems.itemCount > 0) {
LazyColumn(
modifier = Modifier.padding(all = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
lazyPagingItems(pagingItems) { item ->
item?.let {
when (item.mediaType) {
MediaViewType.MOVIE -> {
MovieSearchResultView(
appNavController = appNavController,
result = item as SearchResultMovie
)
}
MediaViewType.TV -> {
TvSearchResultView(
appNavController = appNavController,
result = item as SearchResultTv
)
}
MediaViewType.PERSON -> {
PeopleSearchResultView(
appNavController = appNavController,
result = item as SearchResultPerson
)
}
else ->{}
}
}
}
}
} else {
Column(
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.weight(1f))
Text(
text = stringResource(R.string.no_search_results),
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.CenterHorizontally),
fontSize = 18.sp
)
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
@@ -233,10 +333,15 @@ private fun <T: SortableSearchResult> SearchResultItemView(
@Composable
private fun MovieSearchResultView(
appNavController: NavHostController,
result: SearchResultMovie
result: SearchResultMovie,
service: MoviesService = get(MoviesService::class.java)
) {
val cast = remember { mutableStateOf<List<CastMember>?>(null) }
getCast(result.id, MoviesService(), cast)
LaunchedEffect(Unit) {
service.getCastAndCrew(result.id)
}
val mainViewModel = viewModel<MainViewModel>()
val castMap = remember { mainViewModel.movieCast }
val cast = castMap[result.id]
SearchResultItemView(
appNavController = appNavController,
@@ -247,7 +352,7 @@ private fun MovieSearchResultView(
additionalDetails = {
listOf(
TmdbUtils.releaseYearFromData(result.releaseDate),
cast.value?.joinToString(separator = ", ") { it.name } ?: ""
cast?.joinToString(separator = ", ") { it.name } ?: ""
)
}
)
@@ -256,12 +361,16 @@ private fun MovieSearchResultView(
@Composable
private fun TvSearchResultView(
appNavController: NavHostController,
result: SearchResultTv
result: SearchResultTv,
service: TvService = get(TvService::class.java)
) {
val context = LocalContext.current
val cast = remember { mutableStateOf<List<CastMember>?>(null) }
getCast(result.id, TvService(), cast)
LaunchedEffect(Unit) {
service.getCastAndCrew(result.id)
}
val mainViewModel = viewModel<MainViewModel>()
val castMap = remember { mainViewModel.tvCast }
val cast = castMap[result.id]
SearchResultItemView(
appNavController = appNavController,
@@ -272,7 +381,7 @@ private fun TvSearchResultView(
additionalDetails = {
listOf(
"${TmdbUtils.releaseYearFromData(result.releaseDate)} ${context.getString(R.string.search_result_tv_series)}",
cast.value?.joinToString(separator = ", ") { it.name } ?: ""
cast?.joinToString(separator = ", ") { it.name } ?: ""
)
}
)
@@ -297,78 +406,4 @@ private fun PeopleSearchResultView(
)
}
)
}
private fun searchMovies(
query: String,
searchResults: MutableState<List<SearchResultMovie>>
) {
CoroutineScope(Dispatchers.IO).launch {
val response = SearchService().searchMovies(query)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
searchResults.value = response.body()?.results ?: emptyList()
}
}
}
}
private fun searchTv(
query: String,
searchResults: MutableState<List<SearchResultTv>>
) {
CoroutineScope(Dispatchers.IO).launch {
val response = SearchService().searchTv(query)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
searchResults.value = response.body()?.results ?: emptyList()
}
}
}
}
private fun searchPeople(
query: String,
searchResults: MutableState<List<SearchResultPerson>>
) {
CoroutineScope(Dispatchers.IO).launch {
val response = SearchService().searchPeople(query)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
searchResults.value = response.body()?.results ?: emptyList()
}
}
}
}
private fun searchMulti(
query: String,
searchResults: MutableState<List<SortableSearchResult>>
) {
CoroutineScope(Dispatchers.IO).launch {
val response = SearchService().searchMulti(query)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
searchResults.value = response.body()?.results ?: emptyList()
}
}
}
}
private fun getCast(
id: Int,
detailService: DetailService,
cast: MutableState<List<CastMember>?>
) {
CoroutineScope(Dispatchers.IO).launch {
val response = detailService.getCastAndCrew(id)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
cast.value = response.body()?.cast?.let {
val end = minOf(2, it.size)
it.subList(0, end)
}
}
}
}
}

View File

@@ -0,0 +1,46 @@
package com.owenlejeune.tvtime.ui.viewmodel
import androidx.lifecycle.ViewModel
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.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.utils.SessionManager
import com.owenlejeune.tvtime.utils.types.MediaViewType
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class AccountViewModel: ViewModel(), KoinComponent {
private val listService: ListV4Service by inject()
private val accountService: AccountService by inject()
val listMap = listService.listMap
suspend fun getList(listId: Int) {
listService.getList(listId = listId)
}
suspend fun deleteListItem(listId: Int, itemId: Int, itemType: MediaViewType) {
val removeItemBody = DeleteListItemsItem(itemId, itemType)
listService.deleteListItems(listId, DeleteListItemsBody(listOf(removeItemBody)))
}
suspend fun updateList(listId: Int, body: ListUpdateBody) {
listService.updateList(listId, body)
}
suspend fun addToFavourites(type: MediaViewType, itemId: Int, favourited: Boolean) {
val accountId = SessionManager.currentSession.value?.accountDetails?.value?.id ?: throw Exception("Session must not be null")
accountService.markAsFavorite(accountId, MarkAsFavoriteBody(type, itemId, favourited))
}
suspend fun addToWatchlist(type: MediaViewType, itemId: Int, watchlisted: Boolean) {
val accountId = SessionManager.currentSession.value?.accountDetails?.value?.id ?: throw Exception("Session must not be null")
accountService.addToWatchlist(accountId, WatchlistBody(type, itemId, watchlisted))
}
}

View File

@@ -1,125 +1,28 @@
package com.owenlejeune.tvtime.ui.viewmodel
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.owenlejeune.tvtime.api.tmdb.api.v3.ConfigurationApi
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ConfigurationCountry
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ConfigurationDetails
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ConfigurationJob
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ConfigurationLanguage
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ConfigurationTimezone
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import com.owenlejeune.tvtime.api.tmdb.api.v3.ConfigurationService
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response
class ConfigurationViewModel: ViewModel(), KoinComponent {
companion object {
private const val TAG = "ConfigurationViewModel"
}
private val service: ConfigurationService by inject()
private object Backer {
val detailsConfiguration = mutableStateOf(ConfigurationDetails.Empty)
val countriesConfiguration = mutableStateListOf<ConfigurationCountry>()
val jobsConfiguration = mutableStateListOf<ConfigurationJob>()
val languagesConfiguration = mutableStateListOf<ConfigurationLanguage>()
val primaryTranslationsConfiguration = mutableStateListOf<String>()
val timezonesConfiguration = mutableStateListOf<ConfigurationTimezone>()
}
private val service: ConfigurationApi by inject()
val detailsConfiguration = Backer.detailsConfiguration
val countriesConfiguration = Backer.countriesConfiguration
val jobsConfiguration = Backer.jobsConfiguration
val languagesConfiguration = Backer.languagesConfiguration
val primaryTranslationsConfiguration = Backer.primaryTranslationsConfiguration
val timezonesConfiguration = Backer.timezonesConfiguration
val detailsConfiguration = service.detailsConfiguration
val countriesConfiguration = service.countriesConfiguration
val jobsConfiguration = service.jobsConfiguration
val languagesConfiguration = service.languagesConfiguration
val primaryTranslationsConfiguration = service.primaryTranslationsConfiguration
val timezonesConfiguration = service.timezonesConfiguration
suspend fun getConfigurations() {
getDetailsConfiguration()
getCountriesConfiguration()
getJobsConfiguration()
getLanguagesConfiguration()
getPrimaryTranslationsConfiguration()
getTimezonesConfiguration()
}
suspend fun getDetailsConfiguration() {
getConfiguration(
{ service.getDetailsConfiguration() },
{ detailsConfiguration.value = it }
)
}
suspend fun getCountriesConfiguration() {
getConfiguration(
{ service.getCountriesConfiguration() },
{
countriesConfiguration.clear()
countriesConfiguration.addAll(it)
}
)
}
suspend fun getJobsConfiguration() {
getConfiguration(
{ service.getJobsConfiguration() },
{
jobsConfiguration.clear()
jobsConfiguration.addAll(it)
}
)
}
suspend fun getLanguagesConfiguration() {
getConfiguration(
{ service.getLanguagesConfiguration() },
{
languagesConfiguration.clear()
languagesConfiguration.addAll(it)
}
)
}
suspend fun getPrimaryTranslationsConfiguration() {
getConfiguration(
{ service.getPrimaryTranslationsConfiguration() },
{
primaryTranslationsConfiguration.clear()
primaryTranslationsConfiguration.addAll(it)
}
)
}
suspend fun getTimezonesConfiguration() {
getConfiguration(
{ service.getTimezonesConfiguration() },
{
timezonesConfiguration.clear()
timezonesConfiguration.addAll(it)
}
)
}
private suspend fun <T> getConfiguration(
fetcher: suspend () -> Response<T>,
bodyHandler: (T) -> Unit
) {
val response = fetcher()
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got configuration: $it")
bodyHandler(it)
}
} else {
Log.e(TAG, "Issue getting configuration")
}
service.getDetailsConfiguration()
service.getCountriesConfiguration()
service.getJobsConfiguration()
service.getLanguagesConfiguration()
service.getPrimaryTranslationsConfiguration()
service.getTimezonesConfiguration()
}
}

View File

@@ -0,0 +1,243 @@
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.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
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedItem
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ExternalIds
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Keyword
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatingBody
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.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
class MainViewModel: ViewModel(), KoinComponent {
private val movieService: MoviesService by inject()
private val tvService: TvService by inject()
private val peopleService: PeopleService by inject()
val detailMovies = movieService.detailMovies
val movieImages = movieService.images
val movieCast = movieService.cast
val movieCrew = movieService.crew
val movieVideos = movieService.videos
val movieReviews = movieService.reviews
val movieKeywords = movieService.keywords
val movieWatchProviders = movieService.watchProviders
val movieExternalIds = movieService.externalIds
val movieReleaseDates = movieService.releaseDates
val similarMovies = movieService.similar
val movieAccountStates = movieService.accountStates
val detailedTv = tvService.detailTv
val tvImages = tvService.images
val tvCast = tvService.cast
val tvCrew = tvService.crew
val tvVideos = tvService.videos
val tvReviews = tvService.reviews
val tvKeywords = tvService.keywords
val tvWatchProviders = tvService.watchProviders
val tvExternalIds = tvService.externalIds
val tvContentRatings = tvService.contentRatings
val tvSeasons = tvService.seasons
val similarTv = tvService.similar
val tvAccountStates = tvService.accountStates
val peopleMap = peopleService.peopleMap
val peopleCastMap = peopleService.castMap
val peopleCrewMap = peopleService.crewMap
val peopleImagesMap = peopleService.imagesMap
val peopleExternalIdsMap = peopleService.externalIdsMap
private fun <T> providesForType(type: MediaViewType, movies: () -> T, tv: () -> T): T {
return when (type) {
MediaViewType.MOVIE -> movies()
MediaViewType.TV -> tv()
else -> throw ViewableMediaTypeException(type) // shouldn't happen
}
}
fun produceDetailsFor(type: MediaViewType): Map<Int, DetailedItem> {
return providesForType(type, { detailMovies }, { detailedTv })
}
fun produceImagesFor(type: MediaViewType): Map<Int, ImageCollection> {
return providesForType(type, { movieImages }, { tvImages })
}
fun produceExternalIdsFor(type: MediaViewType): Map<Int, ExternalIds> {
return providesForType(type, { movieExternalIds }, { tvExternalIds })
}
fun produceKeywordsFor(type: MediaViewType): Map<Int, List<Keyword>> {
return providesForType(type, { movieKeywords }, { tvKeywords })
}
fun produceCastFor(type: MediaViewType): Map<Int, List<CastMember>> {
return providesForType(type, { movieCast }, { tvCast })
}
fun produceCrewFor(type: MediaViewType): Map<Int, List<CrewMember>> {
return providesForType(type, { movieCrew }, { tvCrew })
}
fun produceVideosFor(type: MediaViewType): Map<Int, List<Video>> {
return providesForType(type, { movieVideos }, { tvVideos })
}
fun produceWatchProvidersFor(type: MediaViewType): Map<Int, WatchProviders> {
return providesForType(type, { movieWatchProviders }, { tvWatchProviders })
}
fun produceReviewsFor(type: MediaViewType): Map<Int, List<Review>> {
return providesForType(type, { movieReviews }, { tvReviews })
}
fun produceSimilarContentFor(type: MediaViewType): Map<Int, Flow<PagingData<TmdbItem>>> {
return providesForType(type, { similarMovies }, { similarTv })
}
fun produceAccountStatesFor(type: MediaViewType): Map<Int, AccountStates> {
return providesForType(type, { movieAccountStates }, { tvAccountStates} )
}
suspend fun getById(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.getById(id)
MediaViewType.TV -> tvService.getById(id)
MediaViewType.PERSON -> peopleService.getPerson(id)
else -> {}
}
}
suspend fun getImages(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.getImages(id)
MediaViewType.TV -> tvService.getImages(id)
MediaViewType.PERSON -> peopleService.getImages(id)
else -> {}
}
}
suspend fun getCastAndCrew(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.getCastAndCrew(id)
MediaViewType.TV -> tvService.getCastAndCrew(id)
MediaViewType.PERSON -> tvService.getCastAndCrew(id)
else -> {}
}
}
suspend fun getVideos(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.getVideos(id)
MediaViewType.TV -> tvService.getVideos(id)
else -> {}
}
}
suspend fun getReviews(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.getReviews(id)
MediaViewType.TV -> tvService.getReviews(id)
else -> {}
}
}
suspend fun getKeywords(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.getKeywords(id)
MediaViewType.TV -> tvService.getKeywords(id)
else -> {}
}
}
suspend fun getWatchProviders(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.getWatchProviders(id)
MediaViewType.TV -> tvService.getWatchProviders(id)
else -> {}
}
}
suspend fun getExternalIds(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.getExternalIds(id)
MediaViewType.TV -> tvService.getExternalIds(id)
MediaViewType.PERSON -> peopleService.getExternalIds(id)
else -> {}
}
}
suspend fun getAccountStates(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.getAccountStates(id)
MediaViewType.TV -> tvService.getAccountStates(id)
else -> {}
}
}
suspend fun postRating(id: Int, rating: Float, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.postRating(id, RatingBody(rating))
MediaViewType.TV -> tvService.postRating(id, RatingBody(rating))
else -> {}
}
}
suspend fun deleteRating(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.deleteRating(id)
MediaViewType.TV -> tvService.deleteRating(id)
else -> {}
}
}
fun getSimilar(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> {
similarMovies[id] = Pager(PagingConfig(pageSize = 1)) {
SimilarMoviesSource(id)
}.flow.cachedIn(viewModelScope)
}
MediaViewType.TV -> {
similarTv[id] = Pager(PagingConfig(pageSize = 1)) {
SimilarTvSource(id)
}.flow.cachedIn(viewModelScope)
}
else -> {}
}
}
suspend fun getReleaseDates(id: Int) {
movieService.getReleaseDates(id)
}
suspend fun getContentRatings(id: Int) {
tvService.getContentRatings(id)
}
suspend fun getSeason(seriesId: Int, seasonId: Int) {
tvService.getSeason(seriesId, seasonId)
}
}

View File

@@ -13,20 +13,22 @@ 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() {
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(MoviesService(), { s, p -> s.getPopular(p) }, PopularMoviesVM::class.java.simpleName)
object TopRatedMoviesVM: MediaTabViewModel(MoviesService(), { s, p -> s.getTopRated(p) }, TopRatedMoviesVM::class.java.simpleName)
object NowPlayingMoviesVM: MediaTabViewModel(MoviesService(), { s, p -> s.getNowPlaying(p) }, NowPlayingMoviesVM::class.java.simpleName)
object UpcomingMoviesVM: MediaTabViewModel(MoviesService(), { s, p -> s.getUpcoming(p) }, UpcomingMoviesVM::class.java.simpleName)
object PopularTvVM: MediaTabViewModel(TvService(), { s, p -> s.getPopular(p) }, PopularTvVM::class.java.simpleName)
object TopRatedTvVM: MediaTabViewModel(TvService(), { s, p -> s.getTopRated(p) }, TopRatedTvVM::class.java.simpleName)
object AiringTodayTvVM: MediaTabViewModel(TvService(), { s, p -> s.getNowPlaying(p) }, AiringTodayTvVM::class.java.simpleName)
object OnTheAirTvVM: MediaTabViewModel(TvService(), { s, p -> s.getUpcoming(p) }, OnTheAirTvVM::class.java.simpleName)
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

@@ -0,0 +1,70 @@
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.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
class SearchViewModel: ViewModel(), KoinComponent {
private val service: SearchService by inject()
val movieResults = service.movieResults
val tvResults = service.tvResults
val peopleResults = service.peopleResults
val multiResults = service.multiResults
fun resetResults() {
movieResults.value = null
tvResults.value = null
peopleResults.value = null
multiResults.value = null
}
fun searchFor(query: String, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> searchForMovies(query)
MediaViewType.TV -> searchForTv(query)
MediaViewType.PERSON -> searchForPeople(query)
MediaViewType.MIXED -> searchMulti(query)
else -> {}
}
}
fun searchForMovies(query: String) {
movieResults.value = createPagingSource(viewModelScope) { service.searchMovies(query, it) }
}
fun searchForTv(query: String) {
tvResults.value = createPagingSource(viewModelScope) { service.searchTv(query, it) }
}
fun searchForPeople(query: String) {
peopleResults.value = createPagingSource(viewModelScope) { service.searchPeople(query, it) }
}
fun searchMulti(query: String) {
multiResults.value = createPagingSource(viewModelScope) { 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)
}
}

View File

@@ -8,6 +8,7 @@ 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
@@ -30,12 +31,11 @@ import java.nio.charset.StandardCharsets
object SessionManager: KoinComponent {
private val preferences: AppPreferences by inject()
private val authenticationService: AuthenticationService by inject()
private val authenticationV4Service: AuthenticationV4Service by inject()
val currentSession = mutableStateOf<Session?>(null)
private val authenticationService by lazy { TmdbClient().createAuthenticationService() }
private val authenticationV4Service by lazy { TmdbClient().createV4AuthenticationService() }
class AuthorizedSessionValues(
@SerializedName("session_id") val sessionId: String,
@SerializedName("access_token") val accessToken: String,
@@ -132,50 +132,15 @@ object SessionManager: KoinComponent {
}
abstract class Session(val sessionId: String, val isAuthorized: Boolean, val accessToken: String = "", val accountId: String = "") {
// protected open var _ratedMovies: List<RatedMovie> = emptyList()
// val ratedMovies: List<RatedMovie>
// get() = _ratedMovies
val ratedMovies = mutableStateListOf<RatedMovie>()
// protected open var _ratedTvShows: List<RatedTv> = emptyList()
// val ratedTvShows: List<RatedTv>
// get() = _ratedTvShows
val ratedTvShows = mutableStateListOf<RatedTv>()
// protected open var _ratedTvEpisodes: List<RatedEpisode> = emptyList()
// val ratedTvEpisodes: List<RatedEpisode>
// get() = _ratedTvEpisodes
val ratedTvEpisodes = mutableStateListOf<RatedEpisode>()
// protected open var _accountDetails: AccountDetails? = null
// val accountDetails: AccountDetails?
// get() = _accountDetails
val accountDetails = mutableStateOf<AccountDetails?>(null)
// protected open var _accountLists: List<V4AccountList> = emptyList()
// val accountLists: List<V4AccountList>
// get() = _accountLists
val accountLists = mutableStateListOf<AccountList>()
// protected open var _favoriteMovies: List<FavoriteMovie> = emptyList()
// val favoriteMovies: List<FavoriteMovie>
// get() = _favoriteMovies
val favoriteMovies = mutableStateListOf<FavoriteMovie>()
// protected open var _favoriteTvShows: List<FavoriteTvSeries> = emptyList()
// val favoriteTvShows: List<FavoriteTvSeries>
// get() = _favoriteTvShows
val favoriteTvShows = mutableStateListOf<FavoriteTvSeries>()
// protected open var _movieWatchlist: List<WatchlistMovie> = emptyList()
// val movieWatchlist: List<WatchlistMovie>
// get() = _movieWatchlist
val movieWatchlist = mutableStateListOf<WatchlistMovie>()
// protected open var _tvWatchlist: List<WatchlistTvSeries> = emptyList()
// val tvWatchlist: List<WatchlistTvSeries>
// get() = _tvWatchlist
val tvWatchlist = mutableStateListOf<WatchlistTvSeries>()
fun hasRatedMovie(id: Int): Boolean {

View File

@@ -145,14 +145,14 @@ object TmdbUtils {
}
}
fun getMovieRating(releases: MovieReleaseResults?): String {
fun getMovieRating(releases: List<MovieReleaseResults.ReleaseDateResult>?): String {
if (releases == null) {
return ""
}
val currentRegion = Locale.current.language
val certifications = HashMap<String, String>()
releases.releaseDates.forEach { releaseDateResult ->
releases.forEach { releaseDateResult ->
if (releaseDateResult.region == currentRegion || releaseDateResult.region == DEF_REGION) {
val cert = releaseDateResult.releaseDates.firstOrNull { it.certification.isNotEmpty() }
if (cert != null) {
@@ -166,14 +166,14 @@ object TmdbUtils {
return ""
}
fun getTvRating(contentRatings: TvContentRatings?): String {
fun getTvRating(contentRatings: List<TvContentRatings.TvContentRating>?): String {
if (contentRatings == null) {
return ""
}
val currentRegion = Locale.current.language
val certifications = HashMap<String, String>()
contentRatings.results.forEach { contentRating ->
contentRatings.forEach { contentRating ->
if (contentRating.language == currentRegion || contentRating.language == DEF_REGION) {
certifications[contentRating.language] = contentRating.rating
}

View File

@@ -20,4 +20,6 @@ enum class MediaViewType {
return values()[oridinal]
}
}
}
}
class ViewableMediaTypeException(type: MediaViewType): IllegalArgumentException("Media type given: ${type}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV")