diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt index fc61192b..16dc7db8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt @@ -3,15 +3,24 @@ package com.github.damontecres.wholphin.services import android.content.Context import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.NavDrawerItemRepository +import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.HomePageResolvedSettings import com.github.damontecres.wholphin.data.model.HomePageSettings import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowConfigDisplay import com.github.damontecres.wholphin.data.model.HomeRowViewOptions +import com.github.damontecres.wholphin.preferences.HomePagePreferences +import com.github.damontecres.wholphin.ui.DefaultItemFields +import com.github.damontecres.wholphin.ui.SlimItemFields +import com.github.damontecres.wholphin.ui.components.getGenreImageMap import com.github.damontecres.wholphin.ui.main.settings.Library import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem import com.github.damontecres.wholphin.ui.toServerString +import com.github.damontecres.wholphin.util.GetGenresRequestHandler +import com.github.damontecres.wholphin.util.GetItemsRequestHandler +import com.github.damontecres.wholphin.util.HomeRowLoadingState import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.serialization.ExperimentalSerializationApi @@ -22,6 +31,12 @@ import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.UUID +import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.SortOrder +import org.jellyfin.sdk.model.api.UserDto +import org.jellyfin.sdk.model.api.request.GetGenresRequest +import org.jellyfin.sdk.model.api.request.GetItemsRequest +import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest import org.jellyfin.sdk.model.serializer.toUUID import timber.log.Timber import java.io.File @@ -36,6 +51,8 @@ class HomeSettingsService private val api: ApiClient, private val userPreferencesService: UserPreferencesService, private val navDrawerItemRepository: NavDrawerItemRepository, + private val latestNextUpService: LatestNextUpService, + private val imageUrlService: ImageUrlService, ) { val jsonParser = Json { @@ -285,6 +302,244 @@ class HomeSettingsService } } + suspend fun fetchDataForRow( + row: HomeRowConfig, + scope: CoroutineScope, + prefs: HomePagePreferences, + userDto: UserDto, + libraries: List, + limit: Int = prefs.maxItemsPerRow, + ): List = + when (row) { + is HomeRowConfig.ContinueWatching -> { + val resume = latestNextUpService.getResume(userDto.id, limit, true) + listOf( + HomeRowLoadingState.Success( + title = context.getString(R.string.continue_watching), + items = resume, + viewOptions = row.viewOptions, + ), + ) + } + + is HomeRowConfig.NextUp -> { + val nextUp = + latestNextUpService.getNextUp( + userDto.id, + limit, + prefs.enableRewatchingNextUp, + false, + ) + listOf( + HomeRowLoadingState.Success( + title = context.getString(R.string.next_up), + items = nextUp, + viewOptions = row.viewOptions, + ), + ) + } + + is HomeRowConfig.ContinueWatchingCombined -> { + val resume = + latestNextUpService.getResume(userDto.id, limit, true) + val nextUp = + latestNextUpService.getNextUp( + userDto.id, + limit, + prefs.enableRewatchingNextUp, + false, + ) + + listOf( + HomeRowLoadingState.Success( + title = context.getString(R.string.continue_watching), + items = + latestNextUpService.buildCombined( + resume, + nextUp, + ), + viewOptions = row.viewOptions, + ), + ) + } + + is HomeRowConfig.Genres -> { + val request = + GetGenresRequest( + parentId = row.parentId, + userId = userDto.id, + limit = limit, + ) + val items = + GetGenresRequestHandler + .execute(api, request) + .content.items + val genreIds = items.map { it.id } + val genreImages = + getGenreImageMap( + api = api, + scope = scope, + imageUrlService = imageUrlService, + genres = genreIds, + parentId = row.parentId, + includeItemTypes = null, + cardWidthPx = null, + ) + val genres = + items.map { + BaseItem(it, false, genreImages[it.id]) + } + + val name = + libraries + .firstOrNull { it.itemId == row.parentId } + ?.name + val title = + name?.let { context.getString(R.string.genres_in, it) } + ?: context.getString(R.string.genres) + listOf( + HomeRowLoadingState.Success( + title, + genres, + viewOptions = row.viewOptions, + ), + ) + } + + is HomeRowConfig.RecentlyAdded -> { + val name = + libraries + .firstOrNull { it.itemId == row.parentId } + ?.name + val title = + name?.let { context.getString(R.string.recently_added_in, it) } + ?: context.getString(R.string.recently_added) + val request = + GetLatestMediaRequest( + fields = SlimItemFields, + imageTypeLimit = 1, + parentId = row.parentId, + groupItems = true, + limit = limit, + isPlayed = null, // Server will handle user's preference + ) + val latest = + api.userLibraryApi + .getLatestMedia(request) + .content + .map { BaseItem.Companion.from(it, api, true) } + .let { + HomeRowLoadingState.Success( + title, + it, + row.viewOptions, + ) + } + listOf(latest) + } + + is HomeRowConfig.RecentlyReleased -> { + val name = + libraries + .firstOrNull { it.itemId == row.parentId } + ?.name + val title = + name?.let { + context.getString(R.string.recently_released_in, it) + } ?: context.getString(R.string.recently_released) + val request = + GetItemsRequest( + parentId = row.parentId, + limit = limit, + sortBy = listOf(ItemSortBy.PREMIERE_DATE), + sortOrder = listOf(SortOrder.DESCENDING), + fields = DefaultItemFields, + recursive = true, + ) + GetItemsRequestHandler + .execute(api, request) + .content.items + .map { BaseItem.Companion.from(it, api, true) } + .let { + listOf( + HomeRowLoadingState.Success( + title, + it, + row.viewOptions, + ), + ) + } + } + + is HomeRowConfig.ByParent -> { + val request = + GetItemsRequest( + userId = userDto.id, + parentId = row.parentId, + recursive = row.recursive, + sortBy = row.sort?.let { listOf(it.sort) }, + sortOrder = row.sort?.let { listOf(it.direction) }, + limit = limit, + fields = DefaultItemFields, + ) + val name = + api.userLibraryApi + .getItem(itemId = row.parentId) + .content.name + GetItemsRequestHandler + .execute(api, request) + .content.items + .map { BaseItem(it, true) } + .let { + listOf( + HomeRowLoadingState.Success( + name ?: context.getString(R.string.collection), + it, + row.viewOptions, + ), + ) + } + } + + is HomeRowConfig.GetItems -> { + val request = + row.getItems.let { + if (it.limit == null) { + it.copy( + userId = userDto.id, + limit = limit, + ) + } else { + it.copy( + userId = userDto.id, + ) + } + } + val name = + if (row.name == null && request.parentId != null) { + // If a name was not provided, use the parent's name if available + api.userLibraryApi + .getItem(itemId = request.parentId!!) + .content.name + } else { + row.name + } + GetItemsRequestHandler + .execute(api, request) + .content.items + .map { BaseItem.Companion.from(it, api, true) } + .let { + listOf( + HomeRowLoadingState.Success( + name ?: context.getString(R.string.collection), + it, + row.viewOptions, + ), + ) + } + } + } + companion object { const val DISPLAY_PREF_ID = "default" const val CUSTOM_PREF_ID = "home_settings" diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt index 9d6f662b..84f36f02 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt @@ -41,6 +41,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -111,6 +112,7 @@ class GenreViewModel val genreToUrl = getGenreImageMap( api = api, + scope = viewModelScope, imageUrlService = imageUrlService, genres = genres.map { it.id }, parentId = itemId, @@ -141,8 +143,9 @@ class GenreViewModel } } -suspend fun ViewModel.getGenreImageMap( +suspend fun getGenreImageMap( api: ApiClient, + scope: CoroutineScope, imageUrlService: ImageUrlService, genres: List, parentId: UUID, @@ -153,7 +156,7 @@ suspend fun ViewModel.getGenreImageMap( val semaphore = Semaphore(4) genres .map { genreId -> - viewModelScope.async(Dispatchers.IO) { + scope.async(Dispatchers.IO) { semaphore.withPermit { val item = GetItemsRequestHandler diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt index 8f148886..98074c09 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt @@ -14,20 +14,12 @@ import com.github.damontecres.wholphin.data.model.HomePageSettings import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowConfigDisplay import com.github.damontecres.wholphin.data.model.HomeRowViewOptions -import com.github.damontecres.wholphin.preferences.HomePagePreferences import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.HomeSettingsService -import com.github.damontecres.wholphin.services.ImageUrlService -import com.github.damontecres.wholphin.services.LatestNextUpService import com.github.damontecres.wholphin.services.UserPreferencesService -import com.github.damontecres.wholphin.ui.DefaultItemFields -import com.github.damontecres.wholphin.ui.SlimItemFields -import com.github.damontecres.wholphin.ui.components.getGenreImageMap import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem import com.github.damontecres.wholphin.ui.showToast -import com.github.damontecres.wholphin.util.GetGenresRequestHandler -import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.LoadingState import dagger.hilt.android.lifecycle.HiltViewModel @@ -36,15 +28,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.ItemSortBy -import org.jellyfin.sdk.model.api.SortOrder -import org.jellyfin.sdk.model.api.UserDto -import org.jellyfin.sdk.model.api.request.GetGenresRequest -import org.jellyfin.sdk.model.api.request.GetItemsRequest -import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -54,14 +38,11 @@ class HomeSettingsViewModel @Inject constructor( @param:ApplicationContext private val context: Context, - private val api: ApiClient, private val homeSettingsService: HomeSettingsService, private val serverRepository: ServerRepository, private val userPreferencesService: UserPreferencesService, private val navDrawerItemRepository: NavDrawerItemRepository, private val backdropService: BackdropService, - private val latestNextUpService: LatestNextUpService, - private val imageUrlService: ImageUrlService, ) : ViewModel() { private val _state = MutableStateFlow(HomePageSettingsState.EMPTY) val state: StateFlow = _state @@ -102,12 +83,21 @@ class HomeSettingsViewModel val rows = serverRepository.currentUserDto.value?.let { userDto -> val prefs = userPreferencesService.getCurrent().appPreferences.homePagePreferences - state.value.rows - .map { it.config } - .map { row -> - // TODO parallelize - parseRow(prefs, userDto, row, limit) - }.flatten() + state.value.let { state -> + state.rows + .map { it.config } + .map { row -> + // TODO parallelize + homeSettingsService.fetchDataForRow( + row = row, + scope = viewModelScope, + prefs = prefs, + userDto = userDto, + libraries = state.libraries, + limit = limit, + ) + }.flatten() + } } rows?.let { rows -> _state.update { @@ -116,241 +106,6 @@ class HomeSettingsViewModel } } - private suspend fun parseRow( - prefs: HomePagePreferences, - userDto: UserDto, - row: HomeRowConfig, - limit: Int, - ): List = - when (row) { - is HomeRowConfig.ContinueWatching -> { - val resume = latestNextUpService.getResume(userDto.id, limit, true) - listOf( - HomeRowLoadingState.Success( - title = context.getString(R.string.continue_watching), - items = resume, - viewOptions = row.viewOptions, - ), - ) - } - - is HomeRowConfig.NextUp -> { - val nextUp = - latestNextUpService.getNextUp( - userDto.id, - limit, - prefs.enableRewatchingNextUp, - false, - ) - listOf( - HomeRowLoadingState.Success( - title = context.getString(R.string.next_up), - items = nextUp, - viewOptions = row.viewOptions, - ), - ) - } - - is HomeRowConfig.ContinueWatchingCombined -> { - val resume = - latestNextUpService.getResume(userDto.id, limit, true) - val nextUp = - latestNextUpService.getNextUp( - userDto.id, - limit, - prefs.enableRewatchingNextUp, - false, - ) - - listOf( - HomeRowLoadingState.Success( - title = context.getString(R.string.continue_watching), - items = - latestNextUpService.buildCombined( - resume, - nextUp, - ), - viewOptions = row.viewOptions, - ), - ) - } - - is HomeRowConfig.Genres -> { - val request = - GetGenresRequest( - parentId = row.parentId, - userId = userDto.id, - limit = limit, - ) - val items = - GetGenresRequestHandler - .execute(api, request) - .content.items - val genreIds = items.map { it.id } - val genreImages = - getGenreImageMap( - api = api, - imageUrlService = imageUrlService, - genres = genreIds, - parentId = row.parentId, - includeItemTypes = null, - cardWidthPx = null, - ) - val genres = - items.map { - BaseItem(it, false, genreImages[it.id]) - } - - val name = - _state.value.libraries - .firstOrNull { it.itemId == row.parentId } - ?.name - val title = - name?.let { context.getString(R.string.genres_in, it) } - ?: context.getString(R.string.genres) - listOf( - HomeRowLoadingState.Success( - title, - genres, - viewOptions = row.viewOptions, - ), - ) - } - - is HomeRowConfig.RecentlyAdded -> { - val name = - _state.value.libraries - .firstOrNull { it.itemId == row.parentId } - ?.name - val title = - name?.let { context.getString(R.string.recently_added_in, it) } - ?: context.getString(R.string.recently_added) - val request = - GetLatestMediaRequest( - fields = SlimItemFields, - imageTypeLimit = 1, - parentId = row.parentId, - groupItems = true, - limit = limit, - isPlayed = null, // Server will handle user's preference - ) - val latest = - api.userLibraryApi - .getLatestMedia(request) - .content - .map { BaseItem.Companion.from(it, api, true) } - .let { - HomeRowLoadingState.Success( - title, - it, - row.viewOptions, - ) - } - listOf(latest) - } - - is HomeRowConfig.RecentlyReleased -> { - val name = - _state.value.libraries - .firstOrNull { it.itemId == row.parentId } - ?.name - val title = - name?.let { - context.getString(R.string.recently_released_in, it) - } ?: context.getString(R.string.recently_released) - val request = - GetItemsRequest( - parentId = row.parentId, - limit = limit, - sortBy = listOf(ItemSortBy.PREMIERE_DATE), - sortOrder = listOf(SortOrder.DESCENDING), - fields = DefaultItemFields, - recursive = true, - ) - GetItemsRequestHandler - .execute(api, request) - .content.items - .map { BaseItem.Companion.from(it, api, true) } - .let { - listOf( - HomeRowLoadingState.Success( - title, - it, - row.viewOptions, - ), - ) - } - } - - is HomeRowConfig.ByParent -> { - val request = - GetItemsRequest( - userId = userDto.id, - parentId = row.parentId, - recursive = row.recursive, - sortBy = row.sort?.let { listOf(it.sort) }, - sortOrder = row.sort?.let { listOf(it.direction) }, - limit = limit, - fields = DefaultItemFields, - ) - val name = - api.userLibraryApi - .getItem(itemId = row.parentId) - .content.name - GetItemsRequestHandler - .execute(api, request) - .content.items - .map { BaseItem(it, true) } - .let { - listOf( - HomeRowLoadingState.Success( - name ?: context.getString(R.string.collection), - it, - row.viewOptions, - ), - ) - } - } - - is HomeRowConfig.GetItems -> { - val request = - row.getItems.let { - if (it.limit == null) { - it.copy( - userId = userDto.id, - limit = limit, - ) - } else { - it.copy( - userId = userDto.id, - ) - } - } - val name = - if (row.name == null && request.parentId != null) { - // If a name was not provided, use the parent's name if available - api.userLibraryApi - .getItem(itemId = request.parentId!!) - .content.name - } else { - row.name - } - GetItemsRequestHandler - .execute(api, request) - .content.items - .map { BaseItem.Companion.from(it, api, true) } - .let { - listOf( - HomeRowLoadingState.Success( - name ?: context.getString(R.string.collection), - it, - row.viewOptions, - ), - ) - } - } - } - private fun List.move( direction: MoveDirection, index: Int,