diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9a9cfa76..5e935349 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -103,6 +103,7 @@ android { buildTypes { release { isMinifyEnabled = false + signingConfig = signingConfigs.getByName("debug") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", diff --git a/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt index cba14dbd..91f52a46 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt @@ -90,6 +90,9 @@ data class BaseItem( } else { api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY) } + } else if (dto.imageTags == null || dto.imageTags!![ImageType.PRIMARY] == null) { + // TODO is this a bad assumption? + null } else { api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY) } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt index d9344a94..8c361c6c 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt @@ -18,7 +18,7 @@ sealed class AppColors private constructor() { } } -const val DEFAULT_PAGE_SIZE = 50 +const val DEFAULT_PAGE_SIZE = 100 val DefaultItemFields = listOf( diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt index e3494cc4..685e4d47 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt @@ -13,11 +13,16 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.RectangleShape import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.tv.material3.Card @@ -30,6 +35,7 @@ import com.github.damontecres.dolphin.ui.isNotNullOrBlank @Composable fun BannerCard( + name: String?, imageUrl: String?, onClick: () -> Unit, onLongClick: () -> Unit, @@ -41,19 +47,39 @@ fun BannerCard( aspectRatio: Float = 16f / 9, interactionSource: MutableInteractionSource? = null, ) { + var imageError by remember { mutableStateOf(false) } Card( modifier = modifier.size(cardHeight * aspectRatio, cardHeight), onClick = onClick, onLongClick = onLongClick, interactionSource = interactionSource, ) { - Box(modifier = Modifier.fillMaxSize()) { - AsyncImage( - model = imageUrl, - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier.fillMaxSize(), - ) + Box( + modifier = + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceVariant), + ) { + if (!imageError && imageUrl.isNotNullOrBlank()) { + AsyncImage( + model = imageUrl, + contentDescription = null, + contentScale = ContentScale.Fit, + onError = { imageError = true }, + modifier = Modifier.fillMaxSize(), + ) + } else { + Text( + text = name ?: "", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + modifier = + Modifier + .padding(16.dp) + .align(Alignment.Center), + ) + } if (played || cornerText.isNotNullOrBlank()) { Row( horizontalArrangement = Arrangement.spacedBy(4.dp), diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/GridCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/GridCard.kt new file mode 100644 index 00000000..9d96d14d --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/GridCard.kt @@ -0,0 +1,117 @@ +package com.github.damontecres.dolphin.ui.cards + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Card +import androidx.tv.material3.CardDefaults +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text +import com.github.damontecres.dolphin.data.model.BaseItem +import com.github.damontecres.dolphin.ui.enableMarquee +import kotlinx.coroutines.delay + +@Composable +fun GridCard( + item: BaseItem?, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + val dto = item?.data + val focused by interactionSource.collectIsFocusedAsState() + val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp) + val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp) + var focusedAfterDelay by remember { mutableStateOf(false) } + + val hideOverlayDelay = 500L + if (focused) { + LaunchedEffect(Unit) { + delay(hideOverlayDelay) + if (focused) { + focusedAfterDelay = true + } else { + focusedAfterDelay = false + } + } + } else { + focusedAfterDelay = false + } + + Column( + verticalArrangement = Arrangement.spacedBy(spaceBetween), + modifier = modifier, + ) { + Card( + modifier = Modifier.fillMaxWidth(), + onClick = onClick, + onLongClick = onLongClick, + interactionSource = interactionSource, + colors = + CardDefaults.colors( + containerColor = Color.Transparent, + ), + ) { + ItemCardImage( + imageUrl = item?.imageUrl, + name = item?.name, +// showOverlay = !focusedAfterDelay, + showOverlay = true, + favorite = dto?.userData?.isFavorite ?: false, + watched = dto?.userData?.played ?: false, + unwatchedCount = dto?.userData?.unplayedItemCount ?: -1, + watchedPercent = dto?.userData?.playedPercentage, + useFallbackText = false, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(2f / 3f) // TODO + .background(MaterialTheme.colorScheme.surfaceVariant), + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(0.dp), + modifier = Modifier.padding(bottom = spaceBelow).fillMaxWidth(), + ) { + Text( + text = item?.name ?: "", + maxLines = 1, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp) + .enableMarquee(focusedAfterDelay), + ) + Text( + text = item?.data?.productionYear?.toString() ?: "", + maxLines = 1, + textAlign = TextAlign.Center, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp) + .enableMarquee(focusedAfterDelay), + ) + } + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt index e47f80aa..3b99f62b 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt @@ -3,6 +3,7 @@ package com.github.damontecres.dolphin.ui.cards import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState @@ -27,10 +28,13 @@ 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.BlendMode import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp @@ -47,6 +51,7 @@ import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.ui.FontAwesome import com.github.damontecres.dolphin.ui.enableMarquee import com.github.damontecres.dolphin.ui.ifElse +import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.util.seasonEpisode import kotlinx.coroutines.delay import org.jellyfin.sdk.model.api.BaseItemKind @@ -161,24 +166,64 @@ fun ItemCardImage( unwatchedCount: Int, watchedPercent: Double?, modifier: Modifier = Modifier, + useFallbackText: Boolean = true, ) { + var imageError by remember { mutableStateOf(false) } Box(modifier = modifier) { - AsyncImage( - model = imageUrl, - contentDescription = name, - contentScale = ContentScale.Fit, - alignment = Alignment.Center, - // TODO error/fallback images - error = null, - fallback = null, - onError = { - Timber.e(it.result.throwable, "Error loading image: $imageUrl") - }, - modifier = - Modifier - .fillMaxSize() - .align(Alignment.TopCenter), - ) + if (!imageError && imageUrl.isNotNullOrBlank()) { + AsyncImage( + model = imageUrl, + contentDescription = name, + contentScale = ContentScale.Fit, + alignment = Alignment.Center, + // TODO error/fallback images + error = null, + fallback = null, + onError = { + Timber.e(it.result.throwable, "Error loading image: $imageUrl") + imageError = true + }, + modifier = + Modifier + .fillMaxSize() + .align(Alignment.TopCenter), + ) + } else { + Box( + modifier = + Modifier + .background(MaterialTheme.colorScheme.surfaceVariant) + .fillMaxSize() + .align(Alignment.TopCenter), + ) { + if (useFallbackText && name.isNotNullOrBlank()) { + Text( + text = name, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 14.sp, + modifier = + Modifier + .padding(8.dp) + .align(Alignment.Center), + ) + } else { + Image( + painter = painterResource(id = R.drawable.video_solid), + contentDescription = null, + contentScale = ContentScale.Fit, + colorFilter = + ColorFilter.tint( + MaterialTheme.colorScheme.onSurfaceVariant, + BlendMode.SrcIn, + ), + modifier = + Modifier + .fillMaxSize(.4f) + .align(Alignment.Center), + ) + } + } + } AnimatedVisibility( visible = showOverlay, enter = fadeIn(), diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt index ffe126ec..3511dc35 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt @@ -116,6 +116,7 @@ fun BannerItemRow( modifier = modifier, cardContent = { index, item, modifier, onClick, onLongClick -> BannerCard( + name = title, imageUrl = item?.imageUrl, aspectRatio = aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat() ?: (16f / 9), diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt index 542c29ab..6727706b 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -44,7 +43,7 @@ import com.github.damontecres.dolphin.R import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.ui.AppColors import com.github.damontecres.dolphin.ui.FontAwesome -import com.github.damontecres.dolphin.ui.cards.ItemCard +import com.github.damontecres.dolphin.ui.cards.GridCard import com.github.damontecres.dolphin.ui.ifElse import com.github.damontecres.dolphin.ui.playback.isBackwardButton import com.github.damontecres.dolphin.ui.playback.isForwardButton @@ -53,7 +52,6 @@ import com.github.damontecres.dolphin.ui.tryRequestFocus import com.github.damontecres.dolphin.util.ExceptionHandler import kotlinx.coroutines.launch import timber.log.Timber -import kotlin.math.max private const val DEBUG = false @@ -63,7 +61,6 @@ fun CardGrid( itemOnClick: (BaseItem) -> Unit, longClicker: (BaseItem) -> Unit, letterPosition: suspend (Char) -> Int, - requestFocus: Boolean, gridFocusRequester: FocusRequester, showJumpButtons: Boolean, modifier: Modifier = Modifier, @@ -79,45 +76,6 @@ fun CardGrid( val zeroFocus = remember { FocusRequester() } var previouslyFocusedIndex by rememberSaveable { mutableIntStateOf(0) } var focusedIndex by rememberSaveable { mutableIntStateOf(initialPosition) } - var focusedIndexOnExit by rememberSaveable { mutableIntStateOf(-1) } - - // Tracks whether the very first requestFocus has run, if the caller isn't requesting focus, - // then the first time will never run - var hasRequestFocusRun by rememberSaveable { mutableStateOf(!requestFocus) } - var savedFocusedIndex by rememberSaveable { mutableIntStateOf(-1) } - - if (DEBUG) { - Timber.d( - "Grid: hasRun=$hasRequestFocusRun, requestFocus=$requestFocus, initialPosition=$initialPosition, focusedIndex=$focusedIndex", - ) - } - - LaunchedEffect(Unit) { - if (!hasRequestFocusRun) { - // On very first composition, if parent wants to focus on the grid, scroll to the item - if (requestFocus && initialPosition >= 0) { - if (DEBUG) { - Timber.d( - "focus on startPosition=$startPosition, from initialPosition=$initialPosition", - ) - } - focusedIndex = startPosition - gridState.scrollToItem(startPosition, 0) - firstFocus.tryRequestFocus() - } - } else { - val index = savedFocusedIndex - if (DEBUG) Timber.d("savedFocusedIndex=$index") - if (index in 0.. @@ -129,12 +87,12 @@ fun CardGrid( } // Wait for a recomposition to focus - LaunchedEffect(alphabetFocus) { - if (alphabetFocus) { - firstFocus.tryRequestFocus() - } - alphabetFocus = false - } +// LaunchedEffect(alphabetFocus) { +// if (alphabetFocus) { +// firstFocus.tryRequestFocus() +// } +// alphabetFocus = false +// } val useBackToJump = true // uiConfig.preferences.interfacePreferences.scrollTopOnBack val showFooter = true // uiConfig.preferences.interfacePreferences.showPositionFooter @@ -169,7 +127,6 @@ fun CardGrid( val newPosition = (gridState.firstVisibleItemIndex + jump).coerceIn(0.. val mod = - if (index == savedFocusedIndex) { - if (DEBUG) Timber.d("Adding firstFocus to itemClickedIndex $index") - Modifier.focusRequester(firstFocus) - } else if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) { + if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) { if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index") - Modifier.focusRequester(firstFocus) + Modifier + .focusRequester(firstFocus) + .focusRequester(gridFocusRequester) } else { Modifier } val item = pager[index] - if (!hasRequestFocusRun && requestFocus && initialPosition >= 0) { - // On very first composition, if parent wants to focus on the grid, do so - LaunchedEffect(Unit) { - if (DEBUG) { - Timber.d( - "non-null focus on startPosition=$startPosition, from initialPosition=$initialPosition", - ) + GridCard( + item = item, + onClick = { + if (item != null) { + focusedIndex = index + itemOnClick.invoke(item) } - // focus on startPosition - gridState.scrollToItem(startPosition, 0) - firstFocus.tryRequestFocus() - hasRequestFocusRun = true - } - } - ItemCard( + }, + onLongClick = { if (item != null) longClicker.invoke(item) }, modifier = mod .ifElse(index == 0, Modifier.focusRequester(zeroFocus)) @@ -314,20 +258,11 @@ fun CardGrid( focusOn(index) positionCallback?.invoke(columns, index) } else if (focusedIndex == index) { - savedFocusedIndex = index - // Was focused on this, so mark unfocused - focusedIndex = -1 +// savedFocusedIndex = index +// // Was focused on this, so mark unfocused +// focusedIndex = -1 } }, - item = item, - onClick = { - if (item != null) { - itemOnClick.invoke(item) - savedFocusedIndex = index - } - }, - onLongClick = { if (item != null) longClicker.invoke(item) }, - cardHeight = null, ) } } @@ -349,12 +284,12 @@ fun CardGrid( .align(Alignment.BottomCenter) .background(AppColors.TransparentBlack50), ) { - val index = - if (focusedIndex >= 0) { - focusedIndex + 1 - } else { - max(savedFocusedIndex, focusedIndexOnExit) + 1 - } + val index = (focusedIndex + 1).takeIf { it > 0 } ?: "?" +// if (focusedIndex >= 0) { +// focusedIndex + 1 +// } else { +// max(savedFocusedIndex, focusedIndexOnExit) + 1 +// } Text( modifier = Modifier.padding(4.dp), color = MaterialTheme.colorScheme.onBackground, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt index b6a62508..65209991 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt @@ -243,7 +243,6 @@ fun CollectionDetails( }, longClicker = {}, letterPosition = { 0 }, - requestFocus = true, gridFocusRequester = gridFocusRequester, showJumpButtons = false, // TODO add preference modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt index f738054f..6927d45b 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt @@ -226,6 +226,7 @@ fun SeriesOverviewContent( ) } BannerCard( + name = episode?.name, imageUrl = episode?.imageUrl, aspectRatio = episode?.data?.primaryImageAspectRatio?.toFloat() diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt index 2253d917..ac9cb6c4 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt @@ -39,7 +39,6 @@ import androidx.tv.material3.Text import coil3.compose.AsyncImage import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.preferences.UserPreferences -import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect import com.github.damontecres.dolphin.ui.cards.BannerCard import com.github.damontecres.dolphin.ui.cards.ItemRow import com.github.damontecres.dolphin.ui.components.DotSeparatedRow @@ -71,7 +70,7 @@ fun HomePage( modifier: Modifier = Modifier, viewModel: HomeViewModel = hiltViewModel(), ) { - OneTimeLaunchedEffect { + LaunchedEffect(Unit) { viewModel.init(preferences) } val loading by viewModel.loadingState.observeAsState(LoadingState.Loading) @@ -96,14 +95,8 @@ fun HomePageContent( var position by rememberSaveable(stateSaver = RowColumnSaver) { mutableStateOf(RowColumn(0, 0)) } + var focusedItem = position.let { homeRows.getOrNull(it.row)?.items?.getOrNull(it.column) } - var focusedItem by remember { - mutableStateOf( - homeRows.getOrNull(0)?.items?.getOrNull( - 0, - ), - ) - } val focusRequester = remember { FocusRequester() } val positionFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { @@ -181,6 +174,7 @@ fun HomePageContent( cardContent = { index, item, cardModifier, onClick, onLongClick -> // TODO better aspect ration handling? BannerCard( + name = item?.data?.seriesName ?: item?.name, imageUrl = item?.imageUrl, aspectRatio = (2f / 3f), cornerText = item?.data?.indexNumber?.let { "E$it" }, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeViewModel.kt index 3b0ce429..f5ff8743 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeViewModel.kt @@ -20,10 +20,12 @@ import org.jellyfin.sdk.api.client.extensions.tvShowsApi import org.jellyfin.sdk.api.client.extensions.userApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userViewsApi +import org.jellyfin.sdk.model.api.UserDto import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest import org.jellyfin.sdk.model.api.request.GetNextUpRequest import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest import timber.log.Timber +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -44,6 +46,7 @@ class HomeViewModel "Error loading home page", ), ) { + Timber.d("init HomeViewModel") val user by api.userApi.getCurrentUser() // val displayPrefs = // api.displayPreferencesApi @@ -72,68 +75,17 @@ class HomeViewModel // ) // } - val latestMediaIncludes = - user.configuration?.orderedViews.orEmpty().toMutableList().apply { - removeAll(user.configuration?.latestItemsExcludes.orEmpty()) - } - - val views by api.userViewsApi.getUserViews() - val homeRows = homeSections .mapNotNull { section -> Timber.Forest.v("Loading section: %s", section.name) when (section) { HomeSection.LATEST_MEDIA -> { - latestMediaIncludes.mapNotNull { viewId -> - val view = - views.items - .firstOrNull { it.id == viewId } - if (view?.collectionType in supportedCollectionTypes) { - val title = - view - ?.name - ?.let { - "Recently Added in $it" - } - val request = - GetLatestMediaRequest( - fields = DefaultItemFields, - imageTypeLimit = 1, - parentId = viewId, - groupItems = true, - limit = limit, - ) - val latest = - api.userLibraryApi - .getLatestMedia(request) - .content - .map { BaseItem.from(it, api, true) } - HomeRow( - section = section, - items = latest, - title = title, - ) - } else { - null - } - } + getLatest(user, limit) } HomeSection.RESUME -> { - val request = - GetResumeItemsRequest( - userId = user.id, - fields = DefaultItemFields, - limit = limit, - includeItemTypes = supportItemKinds, - ) - val items = - api.itemsApi - .getResumeItems(request) - .content - .items - .map { BaseItem.Companion.from(it, api, true) } + val items = getResume(user.id, limit) listOf( HomeRow( section = section, @@ -143,22 +95,12 @@ class HomeViewModel } HomeSection.NEXT_UP -> { - val request = - GetNextUpRequest( - fields = DefaultItemFields, - imageTypeLimit = 1, - parentId = null, - limit = limit, - enableResumable = false, - enableUserData = true, - enableRewatching = preferences.appPreferences.homePagePreferences.enableRewatchingNextUp, - ) val nextUp = - api.tvShowsApi - .getNextUp(request) - .content - .items - .map { BaseItem.Companion.from(it, api, true) } + getNextUp( + user.id, + limit, + preferences.appPreferences.homePagePreferences.enableRewatchingNextUp, + ) listOf( HomeRow( section = section, @@ -186,4 +128,92 @@ class HomeViewModel } } } + + private suspend fun getResume( + userId: UUID, + limit: Int, + ): List { + val request = + GetResumeItemsRequest( + userId = userId, + fields = DefaultItemFields, + limit = limit, + includeItemTypes = supportItemKinds, + ) + val items = + api.itemsApi + .getResumeItems(request) + .content + .items + .map { BaseItem.Companion.from(it, api, true) } + return items + } + + private suspend fun getNextUp( + userId: UUID, + limit: Int, + enableRewatching: Boolean, + ): List { + val request = + GetNextUpRequest( + userId = userId, + fields = DefaultItemFields, + imageTypeLimit = 1, + parentId = null, + limit = limit, + enableResumable = false, + enableUserData = true, + enableRewatching = enableRewatching, + ) + val nextUp = + api.tvShowsApi + .getNextUp(request) + .content + .items + .map { BaseItem.Companion.from(it, api, true) } + return nextUp + } + + private suspend fun getLatest( + user: UserDto, + limit: Int, + ): List { + val latestMediaIncludes = + user.configuration?.orderedViews.orEmpty().toMutableList().apply { + removeAll(user.configuration?.latestItemsExcludes.orEmpty()) + } + + val views by api.userViewsApi.getUserViews() + val rows = + latestMediaIncludes + .mapNotNull { viewId -> views.items.firstOrNull { it.id == viewId } } + .filter { it.collectionType in supportedCollectionTypes } + .map { view -> + val title = + view + ?.name + ?.let { + "Recently Added in $it" + } + val request = + GetLatestMediaRequest( + fields = DefaultItemFields, + imageTypeLimit = 1, + parentId = view.id, + groupItems = true, + limit = limit, + ) + val latest = + api.userLibraryApi + .getLatestMedia(request) + .content + .map { BaseItem.from(it, api, true) } + HomeRow( + section = HomeSection.LATEST_MEDIA, + items = latest, + title = title, + ) + } + return rows + } } diff --git a/app/src/main/res/drawable/video_solid.xml b/app/src/main/res/drawable/video_solid.xml new file mode 100644 index 00000000..dcd5535c --- /dev/null +++ b/app/src/main/res/drawable/video_solid.xml @@ -0,0 +1,5 @@ + + + + +