diff --git a/app-tv/src/androidTest/java/hu/bbara/purefin/tv/home/ui/TvHomeContentTest.kt b/app-tv/src/androidTest/java/hu/bbara/purefin/tv/home/ui/TvHomeContentTest.kt new file mode 100644 index 00000000..7534cb88 --- /dev/null +++ b/app-tv/src/androidTest/java/hu/bbara/purefin/tv/home/ui/TvHomeContentTest.kt @@ -0,0 +1,108 @@ +package hu.bbara.purefin.tv.home.ui + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import hu.bbara.purefin.core.model.Movie +import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem +import hu.bbara.purefin.feature.shared.home.LibraryItem +import hu.bbara.purefin.feature.shared.home.PosterItem +import hu.bbara.purefin.ui.theme.AppTheme +import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.CollectionType +import org.junit.Rule +import org.junit.Test +import java.util.UUID + +class TvHomeContentTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun tvHomeContent_focusesFirstAvailableItemByDefault() { + composeRule.setContent { + AppTheme { + TvHomeContent( + libraries = sampleLibraries(), + libraryContent = sampleLibraryContent(), + continueWatching = sampleContinueWatching(), + nextUp = emptyList(), + onMediaFocused = {}, + onMovieSelected = {}, + onSeriesSelected = {}, + onEpisodeSelected = { _, _, _ -> } + ) + } + } + + composeRule.waitForIdle() + + composeRule.onNodeWithTag(TvHomeInitialFocusTag).assertIsFocused() + } + + private fun sampleContinueWatching(): List { + return listOf( + ContinueWatchingItem( + type = BaseItemKind.MOVIE, + movie = Movie( + id = UUID.fromString("11111111-1111-1111-1111-111111111111"), + libraryId = UUID.fromString("22222222-2222-2222-2222-222222222222"), + title = "Blade Runner 2049", + progress = 42.0, + watched = false, + year = "2017", + rating = "16+", + runtime = "164m", + format = "4K", + synopsis = "Officer K uncovers a secret that sends him searching for Rick Deckard.", + imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", + audioTrack = "ENG 5.1", + subtitles = "ENG", + cast = emptyList() + ) + ) + ) + } + + private fun sampleLibraries(): List { + return listOf( + LibraryItem( + id = UUID.fromString("33333333-3333-3333-3333-333333333333"), + name = "Movies", + type = CollectionType.MOVIES, + posterUrl = "", + isEmpty = false + ) + ) + } + + private fun sampleLibraryContent(): Map> { + val libraryId = UUID.fromString("33333333-3333-3333-3333-333333333333") + + return mapOf( + libraryId to listOf( + PosterItem( + type = BaseItemKind.MOVIE, + movie = Movie( + id = UUID.fromString("44444444-4444-4444-4444-444444444444"), + libraryId = libraryId, + title = "Arrival", + progress = null, + watched = false, + year = "2016", + rating = "12+", + runtime = "116m", + format = "4K", + synopsis = "A linguist works to communicate with mysterious visitors.", + imageUrlPrefix = "https://images.unsplash.com/photo-1446776811953-b23d57bd21aa", + audioTrack = "ENG 5.1", + subtitles = "ENG", + cast = emptyList() + ) + ) + ) + ) + } +} diff --git a/app-tv/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt b/app-tv/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt index 1ef07d35..62f607fb 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.sp import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator import hu.bbara.purefin.common.ui.components.WatchStateIndicator +import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.shared.home.PosterItem import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.api.BaseItemKind @@ -39,6 +40,8 @@ import org.jellyfin.sdk.model.api.BaseItemKind fun PosterCard( item: PosterItem, modifier: Modifier = Modifier, + imageModifier: Modifier = Modifier, + onFocusedItem: (FocusableItem) -> Unit = {}, onMovieSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, @@ -74,7 +77,7 @@ fun PosterCard( PurefinAsyncImage( model = item.imageUrl, contentDescription = null, - modifier = Modifier + modifier = imageModifier .aspectRatio(2f / 3f) .clip(RoundedCornerShape(14.dp)) .border( @@ -83,7 +86,12 @@ fun PosterCard( shape = RoundedCornerShape(14.dp) ) .background(scheme.surfaceVariant) - .onFocusChanged { isFocused = it.isFocused } + .onFocusChanged { + isFocused = it.isFocused + if (it.isFocused) { + onFocusedItem(item) + } + } .clickable(onClick = { openItem(item) }), contentScale = ContentScale.Crop ) diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/TvAppScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/TvAppScreen.kt index eb22e365..81565a84 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/TvAppScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/TvAppScreen.kt @@ -35,13 +35,15 @@ fun TvAppScreen( libraryViewModel: LibraryViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { - var selectedTabIndex by remember { mutableIntStateOf(1) } + val serverUrl by viewModel.serverUrl.collectAsState() val libraries by viewModel.libraries.collectAsState() val continueWatching by viewModel.continueWatching.collectAsState() val nextUp by viewModel.nextUp.collectAsState() val latestLibraryContent by viewModel.latestLibraryContent.collectAsState() val selectedLibraryItems by libraryViewModel.contents.collectAsState() + var selectedTabIndex by remember { mutableIntStateOf(1) } + val tabs = remember(libraries) { buildList { add( @@ -125,6 +127,7 @@ fun TvAppScreen( libraryContent = latestLibraryContent, continueWatching = continueWatching, nextUp = nextUp, + serverUrl = serverUrl, onMovieSelected = viewModel::onMovieSelected, onSeriesSelected = viewModel::onSeriesSelected, onEpisodeSelected = viewModel::onEpisodeSelected, diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/home/TvHomeScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/home/TvHomeScreen.kt index 59195aad..dadc0008 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/home/TvHomeScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/home/TvHomeScreen.kt @@ -2,8 +2,13 @@ package hu.bbara.purefin.tv.home import android.annotation.SuppressLint 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.Modifier import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem +import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.PosterItem @@ -17,19 +22,26 @@ fun TvHomeScreen( libraryContent: Map>, continueWatching: List, nextUp: List, + serverUrl: String, onMovieSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { + var focusableItem by remember { mutableStateOf(null) } + TvHomeContent( libraries = libraries, libraryContent = libraryContent, continueWatching = continueWatching, nextUp = nextUp, + onMediaFocused = { item: FocusableItem -> + focusableItem = item + }, onMovieSelected = onMovieSelected, onSeriesSelected = onSeriesSelected, onEpisodeSelected = onEpisodeSelected, modifier = modifier ) + } diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeContent.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeContent.kt index dbe63e52..f8b8b5f2 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeContent.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeContent.kt @@ -6,46 +6,78 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.MaterialTheme 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.runtime.withFrameNanos import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1 import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2 import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem +import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.PosterItem import org.jellyfin.sdk.model.UUID +internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item" + @Composable fun TvHomeContent( libraries: List, libraryContent: Map>, continueWatching: List, nextUp: List, + onMediaFocused: (FocusableItem) -> Unit, onMovieSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val visibleLibraries = remember(libraries, libraryContent) { libraries.filter { libraryContent[it.id]?.isEmpty() != true } } - val ( nextUpRef, continueWatchingRef ) = remember { FocusRequester.createRefs() } - val libraryRefs = remember(libraryContent) { - libraryContent.keys.associateWith { FocusRequester() } + val libraryRefs = remember(visibleLibraries) { + visibleLibraries.associate { it.id to FocusRequester() } + } + val initialFocusRequester = remember { FocusRequester() } + val firstVisibleLibraryId = visibleLibraries.firstOrNull()?.id + val firstAvailableItemKey = remember( + continueWatching, + nextUp, + firstVisibleLibraryId, + libraryContent + ) { + when { + continueWatching.isNotEmpty() -> continueWatching.first().id + nextUp.isNotEmpty() -> nextUp.first().id + firstVisibleLibraryId != null -> libraryContent[firstVisibleLibraryId]?.firstOrNull()?.id + else -> null + } + } + var initialFocusApplied by remember { mutableStateOf(false) } + + LaunchedEffect(firstAvailableItemKey, initialFocusApplied) { + if (!initialFocusApplied && firstAvailableItemKey != null) { + withFrameNanos { } + initialFocusRequester.requestFocus() + initialFocusApplied = true + } } LazyColumn( modifier = modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.background) + .background(Color.Transparent) ) { item { Spacer(modifier = Modifier.height(8.dp)) @@ -53,8 +85,11 @@ fun TvHomeContent( item { TvContinueWatchingSection( items = continueWatching, + onFocusedItem = onMediaFocused, onMovieSelected = onMovieSelected, onEpisodeSelected = onEpisodeSelected, + firstItemFocusRequester = initialFocusRequester.takeIf { continueWatching.isNotEmpty() }, + firstItemTestTag = TvHomeInitialFocusTag.takeIf { continueWatching.isNotEmpty() }, modifier = Modifier.focusRequester(continueWatchingRef) .focusProperties { down = nextUpRef @@ -67,7 +102,12 @@ fun TvHomeContent( item { TvNextUpSection( items = nextUp, + onFocusedItem = onMediaFocused, onEpisodeSelected = onEpisodeSelected, + firstItemFocusRequester = initialFocusRequester + .takeIf { continueWatching.isEmpty() && nextUp.isNotEmpty() }, + firstItemTestTag = TvHomeInitialFocusTag + .takeIf { continueWatching.isEmpty() && nextUp.isNotEmpty() }, modifier = Modifier.focusRequester(nextUpRef) .focusProperties { up = continueWatchingRef @@ -87,10 +127,21 @@ fun TvHomeContent( title = item.name, items = libraryContent[item.id] ?: emptyList(), action = "See All", + onFocusedItem = onMediaFocused, onMovieSelected = onMovieSelected, onSeriesSelected = onSeriesSelected, onEpisodeSelected = onEpisodeSelected, - modifier = if (ref != null) Modifier.focusRequester(ref) else Modifier + firstItemFocusRequester = initialFocusRequester.takeIf { + continueWatching.isEmpty() && + nextUp.isEmpty() && + item.id == firstVisibleLibraryId + }, + firstItemTestTag = TvHomeInitialFocusTag.takeIf { + continueWatching.isEmpty() && + nextUp.isEmpty() && + item.id == firstVisibleLibraryId + }, + modifier = (if (ref != null) Modifier.focusRequester(ref) else Modifier) .focusProperties { up = nextUpRef libraryRefs.values.dropWhile { it != ref }.drop(1).firstOrNull() @@ -100,4 +151,4 @@ fun TvHomeContent( Spacer(modifier = Modifier.height(8.dp)) } } -} \ No newline at end of file +} diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeSections.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeSections.kt index 115494e3..a3c664bd 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeSections.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeSections.kt @@ -28,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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -41,6 +44,7 @@ import hu.bbara.purefin.common.ui.components.MediaProgressBar import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem +import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.PosterItem import org.jellyfin.sdk.model.UUID @@ -51,8 +55,11 @@ import kotlin.math.nextUp @Composable fun TvContinueWatchingSection( items: List, + onFocusedItem: (FocusableItem) -> Unit = {}, onMovieSelected: (UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, + firstItemFocusRequester: FocusRequester? = null, + firstItemTestTag: String? = null, modifier: Modifier = Modifier ) { if (items.isEmpty()) return @@ -66,9 +73,25 @@ fun TvContinueWatchingSection( contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - itemsIndexed(items = items) { index, item -> + itemsIndexed(items = items, key = { _, item -> item.id }) { index, item -> TvContinueWatchingCard( item = item, + imageModifier = Modifier + .then( + if (index == 0 && firstItemFocusRequester != null) { + Modifier.focusRequester(firstItemFocusRequester) + } else { + Modifier + } + ) + .then( + if (index == 0 && firstItemTestTag != null) { + Modifier.testTag(firstItemTestTag) + } else { + Modifier + } + ), + onFocusedItem = onFocusedItem, onMovieSelected = onMovieSelected, onEpisodeSelected = onEpisodeSelected ) @@ -80,6 +103,8 @@ fun TvContinueWatchingSection( fun TvContinueWatchingCard( item: ContinueWatchingItem, modifier: Modifier = Modifier, + imageModifier: Modifier = Modifier, + onFocusedItem: (FocusableItem) -> Unit = {}, onMovieSelected: (UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, ) { @@ -139,9 +164,14 @@ fun TvContinueWatchingCard( PurefinAsyncImage( model = imageUrl, contentDescription = null, - modifier = Modifier + modifier = imageModifier .fillMaxSize() - .onFocusChanged { isFocused = it.isFocused } + .onFocusChanged { + isFocused = it.isFocused + if (it.isFocused) { + onFocusedItem(item) + } + } .clickable { openItem(item) }, @@ -178,7 +208,10 @@ fun TvContinueWatchingCard( @Composable fun TvNextUpSection( items: List, + onFocusedItem: (FocusableItem) -> Unit = {}, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, + firstItemFocusRequester: FocusRequester? = null, + firstItemTestTag: String? = null, modifier: Modifier = Modifier ) { if (items.isEmpty()) return @@ -192,14 +225,25 @@ fun TvNextUpSection( contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - itemsIndexed( - items = items, - key = { _, item -> item.id } - ) { index, item -> + itemsIndexed(items = items, key = { _, item -> item.id }) { index, item -> TvNextUpCard( item = item, - isFirstItem = index == 0, - isLastItem = index == items.lastIndex, + imageModifier = Modifier + .then( + if (index == 0 && firstItemFocusRequester != null) { + Modifier.focusRequester(firstItemFocusRequester) + } else { + Modifier + } + ) + .then( + if (index == 0 && firstItemTestTag != null) { + Modifier.testTag(firstItemTestTag) + } else { + Modifier + } + ), + onFocusedItem = onFocusedItem, onEpisodeSelected = onEpisodeSelected ) } @@ -210,8 +254,8 @@ fun TvNextUpSection( fun TvNextUpCard( item: NextUpItem, modifier: Modifier = Modifier, - isFirstItem: Boolean = false, - isLastItem: Boolean = false, + imageModifier: Modifier = Modifier, + onFocusedItem: (FocusableItem) -> Unit = {}, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, ) { val scheme = MaterialTheme.colorScheme @@ -219,7 +263,7 @@ fun TvNextUpCard( var isFocused by remember { mutableStateOf(false) } val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale") - val imageUrl = JellyfinImageHelper.finishImageUrl(item.episode.imageUrlPrefix, ImageType.PRIMARY) + val imageUrl = item.imageUrl val cardWidth = 280.dp @@ -253,9 +297,14 @@ fun TvNextUpCard( PurefinAsyncImage( model = imageUrl, contentDescription = null, - modifier = Modifier + modifier = imageModifier .fillMaxSize() - .onFocusChanged { isFocused = it.isFocused } + .onFocusChanged { + isFocused = it.isFocused + if (it.isFocused) { + onFocusedItem(item) + } + } .clickable { openItem(item) }, @@ -287,6 +336,9 @@ fun TvLibraryPosterSection( title: String, items: List, action: String?, + onFocusedItem: (FocusableItem) -> Unit = {}, + firstItemFocusRequester: FocusRequester? = null, + firstItemTestTag: String? = null, modifier: Modifier = Modifier, onMovieSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit, @@ -302,12 +354,25 @@ fun TvLibraryPosterSection( contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - itemsIndexed( - items = items, - key = { _, item -> item.id } - ) { index, item -> + itemsIndexed(items = items, key = { _, item -> item.id }) { index, item -> PosterCard( item = item, + imageModifier = Modifier + .then( + if (index == 0 && firstItemFocusRequester != null) { + Modifier.focusRequester(firstItemFocusRequester) + } else { + Modifier + } + ) + .then( + if (index == 0 && firstItemTestTag != null) { + Modifier.testTag(firstItemTestTag) + } else { + Modifier + } + ), + onFocusedItem = onFocusedItem, onMovieSelected = onMovieSelected, onSeriesSelected = onSeriesSelected, onEpisodeSelected = onEpisodeSelected diff --git a/app/src/main/java/hu/bbara/purefin/app/home/HomeScreen.kt b/app/src/main/java/hu/bbara/purefin/app/home/HomeScreen.kt index 971cd601..03e846fe 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/HomeScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/HomeScreen.kt @@ -13,20 +13,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import hu.bbara.purefin.app.home.ui.HomeContent import hu.bbara.purefin.app.home.ui.HomeTopBar -import hu.bbara.purefin.app.home.ui.homePreviewContinueWatching -import hu.bbara.purefin.app.home.ui.homePreviewLibraries -import hu.bbara.purefin.app.home.ui.homePreviewLibraryContent -import hu.bbara.purefin.app.home.ui.homePreviewNextUp import hu.bbara.purefin.app.home.ui.search.HomeSearchOverlay import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.shared.home.SuggestedItem -import hu.bbara.purefin.ui.theme.AppTheme import org.jellyfin.sdk.model.UUID @OptIn(ExperimentalMaterial3Api::class) @@ -114,29 +108,4 @@ fun HomeScreen( ) } } -} - -@Preview(name = "Home Screen", showBackground = true, widthDp = 412, heightDp = 915) -@Composable -private fun HomeScreenPreview() { - AppTheme(darkTheme = true) { - HomeScreen( - libraries = homePreviewLibraries(), - libraryContent = homePreviewLibraryContent(), - suggestions = emptyList(), - continueWatching = homePreviewContinueWatching(), - nextUp = homePreviewNextUp(), - isRefreshing = false, - onRefresh = {}, - onMovieSelected = {}, - onSeriesSelected = {}, - onEpisodeSelected = { _, _, _ -> }, - onLibrarySelected = {}, - onProfileClick = {}, - onSettingsClick = {}, - onLogoutClick = {}, - selectedTab = 0, - onTabSelected = {} - ) - } -} +} \ No newline at end of file diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/AppViewModel.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/AppViewModel.kt index f35a8ee1..a89a900b 100644 --- a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/AppViewModel.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/AppViewModel.kt @@ -39,6 +39,13 @@ class AppViewModel @Inject constructor( private val _isRefreshing = MutableStateFlow(false) val isRefreshing: StateFlow = _isRefreshing.asStateFlow() + val serverUrl: StateFlow = userSessionRepository.serverUrl + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + "" + ) + val libraries = appContentRepository.libraries.map { libraries -> libraries.map { LibraryItem( diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomeModels.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomeModels.kt index 924be5ea..9cf20d34 100644 --- a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomeModels.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomeModels.kt @@ -88,26 +88,51 @@ data class SuggestedMovie ( ) ) : SuggestedItem +sealed interface FocusableItem { + val imageUrl: String + val primaryText: String + val secondaryText: String + val description: String + val id: UUID + val type: BaseItemKind +} + data class ContinueWatchingItem( - val type: BaseItemKind, + override val type: BaseItemKind, val movie: Movie? = null, - val episode: Episode? = null -) { - val id: UUID = when (type) { + val episode: Episode? = null, +) : FocusableItem { + override val id: UUID = when (type) { BaseItemKind.MOVIE -> movie!!.id BaseItemKind.EPISODE -> episode!!.id else -> throw UnsupportedOperationException("Unsupported item type: $type") } - val primaryText: String = when (type) { + override val primaryText: String = when (type) { BaseItemKind.MOVIE -> movie!!.title BaseItemKind.EPISODE -> episode!!.title else -> throw UnsupportedOperationException("Unsupported item type: $type") } - val secondaryText: String = when (type) { + override val secondaryText: String = when (type) { BaseItemKind.MOVIE -> movie!!.year BaseItemKind.EPISODE -> episode!!.releaseDate else -> throw UnsupportedOperationException("Unsupported item type: $type") } + override val imageUrl: String = when (type) { + BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl( + prefixImageUrl = movie!!.imageUrlPrefix, + imageType = ImageType.PRIMARY + ) + BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl( + prefixImageUrl = episode!!.imageUrlPrefix, + imageType = ImageType.PRIMARY + ) + else -> throw IllegalArgumentException("Invalid type: $type") + } + override val description: String = when (type) { + BaseItemKind.MOVIE -> movie!!.synopsis + BaseItemKind.EPISODE -> episode!!.synopsis + else -> throw UnsupportedOperationException("Unsupported item type: $type") + } val progress: Double = when (type) { BaseItemKind.MOVIE -> movie!!.progress ?: 0.0 BaseItemKind.EPISODE -> episode!!.progress ?: 0.0 @@ -117,10 +142,19 @@ data class ContinueWatchingItem( data class NextUpItem( val episode: Episode -) { - val id: UUID = episode.id - val primaryText: String = episode.title - val secondaryText: String = episode.releaseDate +) : FocusableItem { + override val id: UUID = episode.id + override val type: BaseItemKind + get() = BaseItemKind.EPISODE + override val imageUrl: String + get() = JellyfinImageHelper.finishImageUrl( + prefixImageUrl = episode.imageUrlPrefix, + imageType = ImageType.PRIMARY + ) + override val primaryText: String = episode.title + override val secondaryText: String = episode.releaseDate + override val description: String + get() = episode.synopsis } data class LibraryItem( @@ -132,12 +166,12 @@ data class LibraryItem( ) data class PosterItem( - val type: BaseItemKind, + override val type: BaseItemKind, val movie: Movie? = null, val series: Series? = null, val episode: Episode? = null -) { - val id: UUID = when (type) { +) : FocusableItem { + override val id: UUID = when (type) { BaseItemKind.MOVIE -> movie!!.id BaseItemKind.EPISODE -> episode!!.id BaseItemKind.SERIES -> series!!.id @@ -149,7 +183,7 @@ data class PosterItem( BaseItemKind.SERIES -> series!!.name else -> throw IllegalArgumentException("Invalid type: $type") } - val imageUrl: String = when (type) { + override val imageUrl: String = when (type) { BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl( prefixImageUrl = movie!!.imageUrlPrefix, imageType = ImageType.PRIMARY @@ -164,6 +198,28 @@ data class PosterItem( ) else -> throw IllegalArgumentException("Invalid type: $type") } + override val primaryText: String + get() = when (type) { + BaseItemKind.MOVIE -> movie!!.title + BaseItemKind.EPISODE -> episode!!.title + BaseItemKind.SERIES -> series!!.name + else -> throw IllegalArgumentException("Invalid type: $type") + } + override val secondaryText: String + get() = when (type) { + BaseItemKind.MOVIE -> movie!!.year + BaseItemKind.EPISODE -> episode!!.releaseDate + BaseItemKind.SERIES -> series!!.year + else -> throw IllegalArgumentException("Invalid type: $type") + } + override val description: String + get() = when (type) { + BaseItemKind.MOVIE -> movie!!.synopsis + BaseItemKind.EPISODE -> episode!!.synopsis + BaseItemKind.SERIES -> series!!.synopsis + else -> throw IllegalArgumentException("Invalid type: $type") + } + fun watched() = when (type) { BaseItemKind.MOVIE -> movie!!.watched BaseItemKind.EPISODE -> episode!!.watched