mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 12:42:44 -05:00
add recommended media tabs to account tab
This commit is contained in:
@@ -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)],
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>>
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user