add recommended media tabs to account tab

This commit is contained in:
Owen LeJeune
2023-05-31 16:24:18 -04:00
parent 833c3e17c4
commit 13cf4ff4fb
11 changed files with 265 additions and 5 deletions

View File

@@ -435,7 +435,10 @@ class MainActivity : MonetCompatActivity() {
) {
it.arguments?.let { arguments ->
val (type, title) = if (preferences.multiSearch) {
Pair(MediaViewType.MIXED, stringResource(id = R.string.search_all_title))
Pair(
MediaViewType.MIXED,
stringResource(id = R.string.search_all_title)
)
} else {
Pair(
MediaViewType[arguments.getInt(NavConstants.SEARCH_ID_KEY)],

View File

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

View File

@@ -2,6 +2,8 @@ package com.owenlejeune.tvtime.api.tmdb.api.v4
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteTvSeries
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedTv
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4AccountList
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4AccountResponse
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4RatedMovie
@@ -40,4 +42,9 @@ interface AccountV4Api {
@GET("account/{account_id}/tv/rated")
suspend fun getRatedTvShows(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<V4AccountResponse<V4RatedTv>>
@GET("account/{account_id}/movie/recommendations")
suspend fun getRecommendedMovies(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<V4AccountResponse<RecommendedMovie>>
@GET("account/{account_id}/tv/recommendations")
suspend fun getRecommendedTvSeries(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<V4AccountResponse<RecommendedTv>>
}

View File

@@ -3,6 +3,8 @@ package com.owenlejeune.tvtime.api.tmdb.api.v4
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteTvSeries
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedTv
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4AccountList
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4AccountResponse
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4RatedMovie
@@ -49,4 +51,12 @@ class AccountV4Service {
return service.getRatedTvShows(accountId, page)
}
suspend fun getRecommendedMovies(accountId: String, page: Int = 1): Response<V4AccountResponse<RecommendedMovie>> {
return service.getRecommendedMovies(accountId, page)
}
suspend fun getRecommendedTvSeries(accountId: String, page: Int): Response<V4AccountResponse<RecommendedTv>> {
return service.getRecommendedTvSeries(accountId, page)
}
}

View File

@@ -0,0 +1,39 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
abstract class RecommendedMedia(
id: Int,
posterPath: String,
title: String,
val type: RecommendedType,
@SerializedName("adult")
val isAdult: Boolean,
@SerializedName("backdrop_path")
val backdropPath: String?,
@SerializedName("original_language")
val originalLanguage: String,
@SerializedName("original_title", alternate = ["original_name"])
val originalName: String,
@SerializedName("overview")
val overview: String,
@SerializedName("media_type")
val mediaType: String,
@SerializedName("genre_ids")
val genreIds: List<Int>,
@SerializedName("popularity")
val popularity: Float,
@SerializedName("release_date", alternate = ["first_air_date"])
val releaseDate: String,
@SerializedName("vote_average")
val voteAverage: Float,
@SerializedName("vote_count")
val voteCount: Int
): TmdbItem(id, posterPath, title) {
enum class RecommendedType {
MOVIE,
SERIES
}
}

View File

@@ -0,0 +1,25 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
class RecommendedMovie (
id: Int,
posterPath: String,
title: String,
isAdult: Boolean,
backdropPath: String?,
originalLanguage: String,
originalName: String,
overview: String,
mediaType: String,
genreIds: List<Int>,
popularity: Float,
releaseDate: String,
voteAverage: Float,
voteCount: Int,
@SerializedName("video")
val isVideo: Boolean
): RecommendedMedia(
id, posterPath, title, RecommendedType.MOVIE, isAdult, backdropPath, originalLanguage,
originalName, overview, mediaType, genreIds, popularity, releaseDate, voteAverage, voteCount
)

View File

@@ -0,0 +1,25 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
class RecommendedTv(
id: Int,
posterPath: String,
title: String,
isAdult: Boolean,
backdropPath: String?,
originalLanguage: String,
originalName: String,
overview: String,
mediaType: String,
genreIds: List<Int>,
popularity: Float,
releaseDate: String,
voteAverage: Float,
voteCount: Int,
@SerializedName("origin_country")
val originCountries: List<String>
): RecommendedMedia(
id, posterPath, title, RecommendedType.SERIES, isAdult, backdropPath, originalLanguage,
originalName, overview, mediaType, genreIds, popularity, releaseDate, voteAverage, voteCount
)

View File

@@ -7,6 +7,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4AccountList
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.AccountTabContent
import com.owenlejeune.tvtime.ui.screens.main.RecommendedAccountTabContent
import com.owenlejeune.tvtime.utils.ResourceUtils
import com.owenlejeune.tvtime.utils.SessionManager
import org.koin.core.component.inject
@@ -34,7 +35,7 @@ sealed class AccountTabNavItem(
val AuthorizedItems
get() = listOf(
RatedMovies, RatedTvShows, RatedTvEpisodes, FavoriteMovies, FavoriteTvShows,
MovieWatchlist, TvWatchlist, UserLists
MovieWatchlist, TvWatchlist, UserLists, RecommendedMovies, RecommendedTv
).filter { it.ordinal > -1 }.sortedBy { it.ordinal }
}
@@ -117,12 +118,48 @@ sealed class AccountTabNavItem(
screenContent,
{ SessionManager.currentSession?.accountLists ?: emptyList() },
V4AccountList::class,
0//7
7
)
object RecommendedMovies: AccountTabNavItem(
R.string.recommended_movies_title,
"recommended_movies_route",
R.string.no_recommended_movies,
MediaViewType.MOVIE,
recommendedScreenContent,
{ emptyList() },
V4AccountList::class,
8
)
object RecommendedTv: AccountTabNavItem(
R.string.recommended_tv_title,
"recommended_tv_route",
R.string.no_recommended_tv,
MediaViewType.TV,
recommendedScreenContent,
{ emptyList() },
V4AccountList::class,
9
)
}
private val screenContent: AccountNavComposableFun = { noContentText, appNavController, mediaViewType, listFetchFun, clazz ->
AccountTabContent(noContentText = noContentText, appNavController = appNavController, mediaViewType = mediaViewType, listFetchFun = listFetchFun, clazz = clazz)
AccountTabContent(
noContentText = noContentText,
appNavController = appNavController,
mediaViewType = mediaViewType,
listFetchFun = listFetchFun,
clazz = clazz
)
}
private val recommendedScreenContent: AccountNavComposableFun = { noContentText, appNavController, mediaViewType, _, _ ->
RecommendedAccountTabContent(
noContentText = noContentText,
appNavController = appNavController,
mediaViewType = mediaViewType,
)
}
typealias ListFetchFun = () -> List<Any>

View File

@@ -30,6 +30,7 @@ import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import coil.compose.AsyncImage
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
@@ -40,12 +41,14 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4AccountList
import com.owenlejeune.tvtime.extensions.unlessEmpty
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.PagingPosterGrid
import com.owenlejeune.tvtime.ui.components.RoundedLetterImage
import com.owenlejeune.tvtime.ui.components.SignInDialog
import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem
import com.owenlejeune.tvtime.ui.navigation.ListFetchFun
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.screens.main.tabs.top.ScrollableTabs
import com.owenlejeune.tvtime.ui.viewmodel.RecommendedMediaViewModel
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.CoroutineScope
@@ -259,7 +262,43 @@ fun <T: Any> AccountTabContent(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecommendedAccountTabContent(
noContentText: String,
appNavController: NavHostController,
mediaViewType: MediaViewType,
) {
val viewModel = when (mediaViewType) {
MediaViewType.MOVIE -> RecommendedMediaViewModel.RecommendedMoviesVM
MediaViewType.TV -> RecommendedMediaViewModel.RecommendedTvVM
else -> throw IllegalArgumentException("Media type given: ${mediaViewType}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen
}
val mediaListItems = viewModel.mediaItems.collectAsLazyPagingItems()
if (mediaListItems.itemCount < 1) {
Column {
Spacer(modifier = Modifier.weight(1f))
Text(
modifier = Modifier.fillMaxWidth(),
text = noContentText,
color = MaterialTheme.colorScheme.onBackground,
fontSize = 22.sp,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.weight(1f))
}
} else {
PagingPosterGrid(
lazyPagingItems = mediaListItems,
onClick = { id ->
appNavController.navigate(
"${MainNavItem.DetailView.route}/${mediaViewType}/${id}"
)
}
)
}
}
@Composable
private fun MediaItemRow(
appNavController: NavHostController,

View File

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

View File

@@ -208,4 +208,8 @@
<string name="label_public_list">Public list?</string>
<string name="title_edit_list">Edit List</string>
<string name="search_all_title">All</string>
<string name="recommended_movies_title">Recommended Movies</string>
<string name="no_recommended_movies">No Recommended Movies</string>
<string name="recommended_tv_title">Recommended TV</string>
<string name="no_recommended_tv">No Recommended TV</string>
</resources>