mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 12:42:44 -05:00
refactor data storage model to viewmodels
This commit is contained in:
11
app/src/main/java/com/owenlejeune/tvtime/api/ServiceUtils.kt
Normal file
11
app/src/main/java/com/owenlejeune/tvtime/api/ServiceUtils.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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>>
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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}%"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user